flyme6.0主题授权机制XX手记

flyme6的主题和谐早在一个月前就做了,当然现在也不去维护了。一开始就想写一篇文章了,奈何拖延症有点严重。。。我只讲一下大致的思路,中间的曲折就不说了。前后花了我2天多点的时间。

一开始做这个插件也只是无聊,因为xx了miui就想xx下flyme,之前也有好多网友希望我做个插件。只是flyme的代码混淆的有点厉害,反编译以后基本看不懂那些代码,又谈何破解。

插件开发使用了Android studio,反编译用了jd-gui jeb和apktool,插件基于xposed框架开发。


刚开始hook了N多的方法,虽然实现了初步实现了目标,但感觉不合理,因为不清楚函数的功能,所以我是hook了大量可疑的函数,这里面肯定有很多是用不到的,然后这些函数都是混淆后的,下次更新岂不是要每个都要重新找一遍?
在进一步的分析中,发现flyme主题的主题试用和恢复默认机制都是写在service中的,我直接干掉这些service不就好了么,剩下的就是怎么让他在系统root之后也能试用主题了。(flyme系统设定开启root后无法试用主题)

至于它怎么获取系统的root信息的。一般的将就2中方法,一个是内部判断,一个是调用系统外部的组件来获取root状态。

通过在反编译后的app源码中搜索root相关的关键词(root su xbin之类的)找到了一个名为isroot的方法。

public static boolean isRoot(Context paramContext)
  {
    if (new File("system/xbin/su").exists()) {
      return true;
    }
    Object localObject2 = paramContext.getSystemService("device_states");
    Object localObject1 = localObject2;
    if (localObject2 == null)
    {
      paramContext = paramContext.getSystemService("deivce_states");
      localObject1 = paramContext;
      if (paramContext == null) {
        return false;
      }
    }
    try
    {
      paramContext = localObject1.getClass().getMethod("doCheckState", new Class[] { Integer.TYPE });
      if (paramContext == null) {
        return false;
      }
      paramContext.setAccessible(true);
      paramContext = (Integer)paramContext.invoke(localObject1, new Object[] { Integer.valueOf(1) });
      if (paramContext != null)
      {
        int i = paramContext.intValue();
        if (1 == i) {
          return true;
        }
      }
    }
    catch (Exception paramContext)
    {
      UsageStatusLog.w("Utils", "Exception : " + paramContext.toString() + " - Cause: " + paramContext.getCause());
    }
    return false;
  }

但hook之后发现没卵用。。。进一步分析发现这个方法压根没有被调用。

然后又找到了一个方法

 public static String getDeviceRoot()
  {
    for (int i = 0;; i = 1)
    {
      try
      {
        if (new File("/system/bin/su").exists()) {
          continue;
        }
        boolean bool = new File("/system/xbin/su").exists();
        if (bool) {
          continue;
        }
      }
      catch (Exception localException)
      {
        for (;;)
        {
          UsageStatusLog.w(TAG, "Exception : " + localException.toString() + " - Cause: " + localException.getCause());
        }
      }
      return String.valueOf(i);
    }
  }

hook之后依旧没用。然后我想这是不是调用了系统里的功能实现的,毕竟root权限是设置里设定的,这样更直接一点。

遂查找manifest里面的条目。

看到了一个可疑的权限声明

  <uses-permission android:name="android.permission.ROOT_RECOVERY_STATE" />

既然可能是调用系统的功能,那么调用的方法总是固定的吧,在源码里搜索getSystemService
然后出现了一大坨。。基本都是用来查询电话信息啊wifi信息啊之类窃取隐私的。

经过大量的筛选,发现了一个可以的调用 getSystemService("device_states")

 public static int h(Context arg7) {
        if(ah.H != -1) {
            int v0 = ah.H;
            return v0;
        }

        ah.H = 0;
        Object v0_1 = arg7.getSystemService("device_states");
        try {
            v0_1 = aa.a(v0_1, "doCheckState", new Class[]{Integer.TYPE}, new Object[]{Integer.valueOf(1)});
            if(v0_1 == null) {
                goto label_28;
            }

            if(1 != ((Integer)v0_1).intValue()) {
                goto label_28;
            }

            ah.H = 1;
        }
        catch(Exception v0_2) {
            v0_2.printStackTrace();
        }

    label_28:
        return ah.H;
    }

然后继续看aa.a这个方法

 public static Object a(Object arg3, String arg4, Class[] arg5, Object[] arg6) {
        Object v0;
        try {
            Method v1_4 = z.a().a(arg3.getClass(), arg4, arg5);
            v1_4.setAccessible(true);
            v0 = v1_4.invoke(arg3, arg6);
        }
        catch(IllegalArgumentException v1) {
            v1.printStackTrace();
        }
        catch(IllegalAccessException v1_1) {
            v1_1.printStackTrace();
        }
        catch(NoSuchMethodException v1_2) {
            v1_2.printStackTrace();
        }
        catch(InvocationTargetException v1_3) {
            if(v1_3.getTargetException() == null) {
                return v0;
            }

            v1_3.getTargetException().printStackTrace();
        }

        return v0;
    }

然后深入a方法内调用的z.a这个方法

    static z a() {
        if(z.a == null) {
            Class v1 = z.class;
            __monitor_enter(v1);
            try {
                if(z.a == null) {
                    z.a = new z();
                }

                __monitor_exit(v1);
            }
            catch(Throwable v0) {
                try {
                label_13:
                    __monitor_exit(v1);
                }
                catch(Throwable v0) {
                    goto label_13;
                }

                throw v0;
            }
        }

        return z.a;
    }

不知道这段代码各位看懂了没。。老实说这些高度混淆的代码是不能直接看的。但我不清楚要怎么处理。所以我就hook了试试看,结果就成了。

但是有网友反馈,在重启系统后主题会恢复默认。我想这其中肯定还有其他的检测逻辑来判断授权。既然是开机之后才会这样,我禁用了他的 android.intent.action.BOOT_COMPLETED接收器不就OK了吗?

检查系统恢复主题那一刻的logcat输出,发现flyme和miui一样,什么东西都往logcat里面写。
直接通过关键词查找到了那句输出语句。

 Log.i("check", "resetToSystemTheme");

直接把包含这个命令的方法replace成空

至于那个字体自动恢复,我应用的字体会改名为flymefont.ttf并复制到存放在data/data/com.meizu.customizecenter/font/。
你使用RE查看app的应用数据目录的时候可以观察到。

然后在源码里搜索整个路径,你会找到几句判断这个目录下有没有文件的方法。如果有,则删除之,

  public boolean a()
  {
    boolean bool3 = true;
    boolean bool1 = true;
    Object localObject = new File("data/data/com.meizu.customizecenter/font/");
    boolean bool2 = bool3;
    if (localObject != null)
    {
      bool2 = bool3;
      if (((File)localObject).isDirectory())
      {
        localObject = ((File)localObject).listFiles();
        if ((localObject != null) && (localObject.length == 0)) {}
        int j = localObject.length;
        int i = 0;
        for (;;)
        {
          bool2 = bool1;
          if (i >= j) {
            break;
          }
          File localFile = localObject[i];
          bool2 = bool1;
          if (localFile.exists())
          {
            bool2 = bool1;
            if (!b.a(localFile)) {
              bool2 = false;
            }
          }
          i += 1;
          bool1 = bool2;
        }
      }
    }
    if (bool2)
    {
      b(d.a("system_font"));
      f();
    }
    return bool2;
  }

最好的f方法是最终调用恢复字体的方法的方法。

最终整个插件一共hook了三处,一处是检测root,另外2处是处理主题和字体恢复的函数,因为每次开机,主题美化都会检测一次授权状态。

总结:
1.混淆后的代码难以理解,基本不用去考虑捋清楚他的代码逻辑。基本代码调用层次一多,一层一层看下去就发现自己看不懂了。

2.调用外部接口的方法名,类名或者枚举值是不会被混淆的,因为这些都不是app内部定义的,比如getSystemService这个方法就是个明显的标志。

3.可以观察logcat 找到线索

4.因为app是需要被系统识别的,所以一般一些需要外部调用的东西,比如service的完整包名类名,activity的包名类名是不会混淆的,所以你应该大致的知道你需要找的东西在哪

5.善用关键词搜索