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.善用关键词搜索