MIUI中对与root权限的管理和控制通过两个模块实现:
su 这是一个ELF可执行文件,在系统中的路径为/system/bin/su
Superuser.apk(Superuser.odex) 授权管理app
MIUI中的app提权到root的一般过程为:
某个app通过Runtime.getRuntime().exec()方法执行提权命令”su [options]“
su被执行,将待提权app的相关信息以广播的形式发送出去,并开启local socket服务端等待客户端连接
Superuser.apk中的广播接收器接受到su发送的广播,获得用户的设置,然后将结果通过local socket返回给su
su根据Socket客户端发来的结果继续或者终止提权过程
二、问题定位
由于MIUI的su不容易被逆向分析,所以从分析Superuser.apk(Superuser.odex)入手。MIUI将系统应用程序(/sysetm/app目录下)apk文件中的classes.dex提取出来,进行优化得到odex文件,也存放到/system/app目录下。例如,Superuser这个app就分为两个部分,Superuser.apk和Superuser.odex,与典型的apk文件不同,Superuser.apk中已没有classes.dex了。
查看Superuser.apk中的AndroidManifest.xml,看到了一个比较关键的BroadcastReceiver:
- <span style="font-size:12px;"><?xml version="1.0" encoding="utf-8"?>
- <manifest android:versionCode="24" android:versionName="2.3.6"
- package="com.miui.uac"
- xmlns:android="http://schemas.android.com/apk/res/android">
- ...
- <receiver android:name="SuRequestReceiver">
- <intent-filter>
- <action android:name="com.miui.uac.REQUEST" />
- </intent-filter>
- </receiver>
- ...
- <uses-permission android:name="com.miui.uac.RESPOND" />
- <permission
- android:label="@string/permlab_respond"
- android:name="com.miui.uac.RESPOND"
- android:protectionLevel="signature"
- android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
- android:description="@string/permdesc_respond" /></span>
根据名字大概可以判断,su发来的广播会被此receiver接受并进行处理(后面查看它的smali代码也验证了这个判断)。值得注意的是,这个receiver是对外暴露的,而且 看起来没有受到任何权限的保护。如何利用这一点,有两个思路:
伪造广播,看能否有”意外”作用
伪造广播接收器,实现广播监听或者劫持(虽然从AndroidManifest.xml中来看,com.miui.uac.RESPOND权限可能会成为一个障碍)
(1)伪造广播
尝试使用am发送一条空广播:
$ adb shell am broadcast -a com.miui.uac.REQUEST
授权管理app崩溃了。
查看log发现广播接收器中的数据库查询语句出了问题,应该是缺少参数导致的。看来伪造广播并发送是可行的,但是在此之前,要找到合适的广播参数,让授权管理app不会崩溃。使用baksmali反编译Superuser.apk查看com/miui/uac/SuRequestReceiver.smali的代码:
- <span style="font-size:12px;">.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V
- const/4 v2, 0x0
- const-string v0, "caller_uid" #从intent中得到caller_uid
- invoke-virtual {p2, v0, v2}, Landroid/content/Intent;->getIntExtra(Ljava/lang/String;I)I
- move-result v0
- const-string v1, "desired_uid" #从intent中得到desired_uid
- invoke-virtual {p2, v1, v2}, Landroid/content/Intent;->getIntExtra(Ljava/lang/String;I)I
- move-result v1
- const-string v2, "desired_cmd" #从intent中得到desired_cmd
- invoke-virtual {p2, v2}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
- move-result-object v2
- const-string v3, "socket" #从intent中得到socket
- invoke-virtual {p2, v3}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
- move-result-object v3
- # 从数据库中查询当前app权限配置
- new-instance v4, Lcom/miui/uac/DBHelper;
- invoke-direct {v4, p1}, Lcom/miui/uac/DBHelper;->(Landroid/content/Context;)V
- invoke-virtual {v4, v0, v1, v2}, Lcom/miui/uac/DBHelper;->checkApp(IILjava/lang/String;)Lcom/miui/uac/AppDetails;
- move-result-object v0
- invoke-virtual {v0}, Lcom/miui/uac/AppDetails;->getAllow()I
- move-result v1
- const/4 v2, -0x1
- if-ne v1, v2, :cond_3f
- # 弹框提示用户允许/拒绝当前app提权
- new-instance v0, Landroid/content/Intent;
- const-class v1, Lcom/miui/uac/SuRequest;
- invoke-direct {v0, p1, v1}, Landroid/content/Intent;->(Landroid/content/Context;Ljava/lang/Class;)V
- invoke-virtual {v0, p2}, Landroid/content/Intent;->putExtras(Landroid/content/Intent;)Landroid/content/Intent;
- const/high16 v1, 0x1000
- invoke-virtual {v0, v1}, Landroid/content/Intent;->addFlags(I)Landroid/content/Intent;
- invoke-virtual {p1, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V
- :goto_3b
- invoke-virtual {v4}, Lcom/miui/uac/DBHelper;->close()V
- return-void
- :cond_3f
- # 返回结果给su
- invoke-static {p1, v0, v3}, Lcom/miui/uac/ResponseHelper;->sendResult(Landroid/content/Context;Lcom/miui/uac/AppDetails;Ljava/lang/String;)V
- goto :goto_3b
- .end method</span>
caller_uid, desired_uid很容易明白, 但是desired_cmd是什么形式的就难以琢磨了。不过既然在数据库操作中把desired_cmd传递过去了,先看看数据库中有没有相关信息。
在/data/data/com.miui.uac/databases中有一个数据库文件permissions.sqlite,其中有三个表:
- <span style="font-size:12px;"># pwd
- /datadata/com.miui.uac/databases
- # ls -l
- -rw-rw---- 1 app_32 app_32 288768 Jan 21 17:01 permissions.sqlite
- # sqlite3 permissions.sqlite
- sqlite> .tables
- android_metadata apps logs prefs
- sqlite> .schema apps
- CREATE TABLE apps (_id INTEGER, uid INTEGER, package TEXT, name TEXT,
- exec_uid INTEGER, exec_cmd TEXT, allow INTEGER, PRIMARY KEY (_id),
- UNIQUE (uid,exec_uid,exec_cmd));
- sqlite> select * from apps;
- 1|10058|jackpal.androidterm|终端模拟器|0|/system/bin/sh|1</span>
结合apps表的schema和其中的内容,exec_uid对应前面提到的desired_uid,exec_cmd对应前面提到的desired_cmd。因此,把desired_cmd设置为”/system/bin/sh”就行了。(后来发现,只要不是空字符串就行……)构造以下广播:
- <span style="font-size:12px;">$ adb shell am broadcast -a com.miui.uac.REQUEST \
- > --ei caller_uid 10051 --ei desired_uid 0 --es desired_cmd "/system/bin/sh"</span>
被成功接收,并且授权管理app弹出对话框提示用户进行授权。
使用代码实现的话也比较简单:
- <span style="font-size:12px;"> Intent it = new Intent();
- it.setAction("com.miui.uac.REQUEST");
- // ComponentName c = new ComponentName("com.miui.uac", "com.miui.uac.SuRequestReceiver");
- // it.setComponent(c);
- it.putExtra("caller_uid", 10051);
- it.putExtra("desired_uid", 0);
- it.putExtra("desired_cmd", "/system/bin/sh");
- getApplicationContext().sendBroadcast(it);</span>
自己写一个app注册广播接收器net.yurushao.uactest.FakeSuRequestReceiver:
- <span style="font-size:12px;"><receiver android:name=".FakeSuRequestReceiver" >
- <intent-filter>
- <action android:name="com.miui.uac.REQUEST" />
- </intent-filter>
- </receiver></span>
- <span style="font-size:12px;">public class FakeSuRequestReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context arg0, Intent arg1) {
- // TODO Auto-generated method stub
- System.out.println(arg1.getAction());
- }
- }</span>
选取某个正常app进行提权时,FakeSuRequestReceiver没有任何反应。查看log发现:
Permission Denial: receiving Intent { act=com.miui.uac.REQUEST (has extras) }
to net.yurushao.uactest requires com.miui.uac.RESPOND due to sender null (uid 0)
没有com.miui.uac.RESPOND权限,而这个权限是受签名保护的,因此对su发出广播的监听和劫持都是无法实现。
三、 总结
su发送 的广播可以被伪造,但是不可以被监听或者劫持。至于对广播的伪造,除了能引起授权管理app的崩溃,还没有发现可以被利用的地方。