对于我们Android开发工程师来说,HTML5一点都不陌生,作为第五代超文本标记语言,它对前端工程师吧而言意义重大,几乎是必备技能。此外HTML5的强大之处还在于它可以被应用于Android App开发,可以极大的节省开发成本和时间,提高开发效率!
博主是一直都知道HTML5可以用来开发App,但是真正全部使用HTML5开发的App还是一点都没见过,一直到接到公司的破解任务,破解小米的系统应用:小米应用商店,这下才算是开了眼界!小米应用商店采用的全为HTML5开发完成,加载的页面全部为网页,这里展示下首页的页面分析图:
看到整体的红框对应的是什么控件了么?WebView!一整个的网页呀.....后面你再翻找别的页面分析元素,你会发现相同的结果,所有页面都是由WebView加载的网页!你如果问难道就没有一点不是网页么?你还别说,还真有一个不是网页,那就是上图首页的搜索框了!如图:
从页面结构上就可以看出来,这个搜索框并不是在WebView下的子布局,而是它的兄弟,他们两个有共同的父布局FrameLayout 。这样我们就可以大致的分析出来,小米应用商店有一个共同的父布局,父布局中存在两个主要子布局:WebView和FrameLayout,WebView中用来加载展示请求的网页,FrameLayout中是TextSwitcher控件,该控件主要用来上下滚动TextView。
我们继续看这张界面的元素分析,对于WebView中加载的网页,照样也是可以分析出它的布局结构,如图所示:
我们可以很清楚的看到下面的App应用展示是一条ListView,不过有值得注意的地方的是:在下面的详情中resource-id字段是空的。这里是非常正常,毕竟这里是一个网页,并不是原生的Android界面,自然是没有相应控件ID的!
没有控件ID也就意味着处于无从下手的地步,这里也就展示使用出HTML5开发的App破解难点所在,无法锁定关键控件的ID,同时也无法窥探关键源码,毕竟关键的源码都在服务器后台,你看个锤子哦!
既然如此,那我们到底要怎么去破解?在没有关键控件ID,无法查看关键源码的情况下,针对HTML5开发的App应用逆向破解技巧,下面就赶快让我们来学习吧~
首先有请我们今天的教案对象:小米应用商店(版本:7.5),本次目的是:屏蔽小米应用商店的账号检测!这里感谢小米应用商店的辛苦付出,在此声明本次教案只以学习技术为主,搞破坏是不可以的!下面就让我们开始今天的学习吧!
可能有同学不太明白屏蔽小米应用商店的账号检测是什么操作,这里解释一下:如果你使用的是小米手机,那么你在使用小米应用商店(7.5版本)的时候会发现并没有一个登录的页面,所有的登录操作全部放在“我的小米”系统App中进行,当你在“我的小米”完成登录后,来到小米应用商店对某一款App进行评论的时候,这里会自动检测你的小米账号登录状态,如果已经登录那么就会让你直接进行评论,反之如果你没有登录的话,那么就会弹出来提示登录后才能评论!
如图:如果已经登录,App详情页面点击“我要评论”会弹出评星和输入框:
如图:如果没有登录,点击“我要评论”会弹出请登录的提示:
所以屏蔽小米应用商店的账号检测就是让小米应用商店检测不到我们账号的登录状况,即使我们已经登录了,还是认为我们没有登录!下面就跟着我赶快来学习一下怎么做吧!
首先就是第一个难题:如何找到突破点?按照正常的逆向分析步骤,第一步就是要锁定关键代码,但是这里毕竟是HTML5开发的App,网页的逻辑和布局都在服务器端,想要锁定关键代码可谓是难上加难!难道就没有办法了吗?不会,纵然他是加载的网页,但是总归还是要和本地进行数据传输和交互的,要不然它是怎么知道我这里是否账号登陆了呢!既然存在数据交互,那么必然就有可破解的地方~
逆向分析有个必要的基本要求,那就是你要对你需要逆向的对象知根知底!你要了解它的编码方式,它的编码语言,它的运行环境和开发环境等等好多,也就是说你要比它妈还要了解它才行!这里为什么我们破解HTML5开发的App会感觉很吃力?一个根本原因就是我们不了解HTML5 App的开发模式和开发流程,所以我们才会感觉无从下手!
所以第一步就是我们要弄明白HTML5开发App是如何做到数据交互的?去网上百度一下HTML5与手机本地交互就会明白怎么回事,这里其实并不用了解太多,只需要知道关键一点就好了:@JavascriptInterface注解。
关于@JavascriptInterface注解,如果你用HTML5开发过Android应用,那么你很轻松就会知晓这个注解的作用,像博主这样从来没有用HTML5开发过只限于了解的小伙伴,下面就要好好记笔记:
> 在用HTML5开发Android项目时,会需要js去调用Java中的某些函数达到数据交互、行为指示等目的,其中有两种js与Java交互的方式:
>
> 1:系统提供的webview.addJavascriptInterface
>
> 2:WebViewClient.shouldOverrideUrlLoading
>
> 第一种存在非常大的安全隐患:通过js获取高权限,盗取用户信息、运行病毒代码等等(说的不就是......我可没干过!),所以在4.2及以上版本(API >= 17)新增了@JavascriptInterface注解来提高安全等级!如果Java类中没有注解的方法,js无法调用,并在4.4版本增加了带回调的方法webview.evaluateJavascript(s,valuecallback)。
>
> 第二种暂不讨论,想知道的小伙伴请自行百度。
通过上面的解释我们这里明白了,@JavascriptInterface注解的功能实质上是为了提高网络安全连接,换句话来说就是只有带有@JavascriptInterface注解的Java方法才能被js调用!那么破解点就来了,既然我们要想找到网页与本地数据交互的地方所在,那么这个@JavascriptInterface注解简直就是引路灯啊有没有!
废话少说,使用Jadx打开小米应用商店源码,全局查找“@JavascriptInterface”字符,结果如下:
至于为什么右边的代码看不到,那是因为注解在源码中存在的颜色是淡灰色,这里和结果框的颜色重合了........不过没有关系,我们知道就行了。通过全局查找我们发现在整个小米应用商店中存在有87处引用,也就是意味着js这里可以调用多达87个Java方法。
那么这么多方法那个才是我们想要的?这里就要看经验了,在全部的搜索结果中,我们会发现在一个叫做WebEvent的类中,它的@JavascriptInterface注解是最多的,而且它里面添加@JavascriptInterface的方法中有很多非常关键的信息,比如login()!题外插一句,像这样搜索结果比较多的,如何快速的找到自己需要的关键信息这里是全凭经验拿捏,我个人经验就是看结果展示栏中给出的代码信息,是否存在我想要的关键字来确定。关键字就是你想找的那个字。经验这个东西有时候准有时候不准,总之也七七八八马马虎虎吧。
本次我们的目的是要屏蔽登录账号检测,最重要的关键代码肯定是登录啊!既然WebEvent类中存在标有@JavascriptInterface注解的login()方法,那么想必这里就是js调起的登录方法了,赶快去看看:
@JavascriptInterface
public void login(String str) {
try {
String string = new JSONObject(str).getString("callBack");
if (!TextUtils.isEmpty(string)) {
if (c.yh().yp() == 1) {
String yi = c.yh().yi();
JSONObject jSONObject = new JSONObject();
try {
jSONObject.put(LocaleUtil.INDONESIAN, yi);
sendDataToCallback(string, jSONObject.toString());
} catch (Throwable e) {
ax.e(TAG, "login failed", e);
}
} else if (this.mContext != null && (this.mContext instanceof Activity)) {
c.yh().a((Activity) this.mContext, new x(this, string));
}
}
} catch (Throwable e2) {
ax.e(TAG, "[getUserId] : get input failed", e2);
}
}
我们重点看if判断中的语句:
if (c.yh().yp() == 1) {
String yi = c.yh().yi();
JSONObject jSONObject = new JSONObject();
try {
jSONObject.put(LocaleUtil.INDONESIAN, yi);
sendDataToCallback(string, jSONObject.toString());
} catch (Throwable e) {
ax.e(TAG, "login failed", e);
}
} else if (this.mContext != null && (this.mContext instanceof Activity)) {
c.yh().a((Activity) this.mContext, new x(this, string));
}
首先判断的是c.yh().yp() 方法返回值是不是1,如果等于1的话那么就进入下面的语句,调用c.yh().yi()方法拿到了一个String字符串,把它放入一条Json中,key值为LocaleUtil.INDONESIAN。我们可以看到LocaleUtil.INDONESIAN的值:
key为“id”,接下来就调用了sendDataToCallback()方法并把Json信息放了进去。这里我们需要知道一下这条Key为“id”的数据到底是什么,能够出现在login()方法内想必不会是什么平凡角色!
直接编写Xposed拦截查看,目标类是com.xiaomi.f.a.c,目标方法是yi(),代码如下:
XposedHelpers.findAndHookMethod("com.xiaomi.f.a.c", loadPackageParam.classLoader, "yi", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("小米应用商店:抓到方法com.xiaomi.f.a.c->yi(),返回值为:"+param.getResult());
}
});
运行一下查看拦截信息:
我们立刻就发现这条String字符为加密后的小米ID啊!原来这里是获取了加密后的小米ID进行了自动登录操作,下面我们追根到底,它是如何拿到这条加密后的小米ID呢?去com.xiaomi.f.a.c类下看看:
我们发现返回值是c类中的TO变量,看下这个TO变量是如何被赋值的:
我们发现在c类的a()方法中,TO变量通过传进来的String参数被赋值,那么接着找下去,看看在哪里调用了这个赋值方法a()。这里就需要使用AndroidStudio全局搜索Smali代码了:
发现在h类中被调用,下面去这个h类中看看:
在h类中的run()方法内,我们通过代码:this.ayz.a(string, string2, userData, gT.authToken, gT.security);确定传入的加密后的小米ID为userData,userData的值来自:this.ayz.afR.getUserData(accountsByType[0], "encrypted_user_id");,这个this.ayz.afR变量是之前c类中的AccountManager类对象:
所以这里我们得到的结论是这个用来登录的加密小米ID是通过AccountManager类的getUserData()方法获取的,传入的key值为“encrypted_user_id”,关于这个AccountManager类,这里简单说一下,这是Android系统提供的账号管理类,AccountManager可以为不同类型的账户提供了统一验证管理的方法,处理有效的账户的具体信息而且实现排序,还有一点很重要的是:AccountManager能够为应用生成tokens,这样应用就不须要直接处理password。tokens是能够被复用的而且由AccountManager缓存,可是必须周期性的刷新。应用程序必须在停止工作时丢弃tokens以便让AccountManager知道须要又一次生成tockens。这里不再具体的详解AccountManager类,有需要的小伙伴可以自行查阅资料学习。
我们上面了解了AccountManager类的具体功能,我们从头看h类的run()方法,我们就会发现这里同时也拿取了一个token信息,这个token也就是AccountManager为小米应用商店生成的token值,这也就是说为什么我们没有发现登录密码password被获取,原因就在这里!
通过上面的源码分析,我们已经知道这条加密的小米ID的来源,那么下面我们还拐过来看WebEvent类中的login()方法,之前我们分析了逻辑处理,首先会进行一个if判断,判断的是c.yh().yp() 方法返回值是不是1,如果是1的话那么就会执行登录的动作,看来这个yp()方法应该是整个程序对登录状态的判断标志,我们需要去看看这个判断依据是怎么来的!yp()方法代码如下:
public int yp() {
if (!TextUtils.isEmpty(this.mServiceToken) && !TextUtils.isEmpty(this.mSecurity)) {
return 1;
}
if (this.afR.getAccountsByType("com.xiaomi.unactivated").length != 0) {
return 3;
}
return 2;
}
逻辑还是非常清楚简单,我们来分析看看。我们首先可以看出的就是基本上就是对String字符串的判空,总共会有三种返回数值:1,2,3。返回1的情况是mServiceToken和mSecurity这两个字符串都不为null的时候,这里我们已经知道的是1的标志为可以进行登录。看来mServiceToken和mSecurity这两个字符串其实才是真正的判断依据!同时还有一个3的情况,看下他的判断条件,“unactivated”意思为“未激活的”,也就是说如果取到了未激活的账号信息,那么将返回3,所以3的标志为账号没有激活,那剩下的2标志就很清楚了,表示账号不可以登录。
我们本次的目的就是为了屏蔽应用商店对账号登录情况的检测,现在我们发现了关键登录情况指示方法,下面就很好办了,使用Xposed进行拦截修改:目标类是c类,目标方法是yp(),代码如下:
XposedHelpers.findAndHookMethod("com.xiaomi.f.a.c", loadPackageParam.classLoader, "yp", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("小米应用商店:抓到方法com.xiaomi.f.a.c->yp(),返回值为:登录状态:"+param.getResult());
param.setResult(2);//2表示不可以登录账号
}
});
这里我们把该方法的返回结果设置为2标志,看下是否达到了我们的效果:来到应用详情页,点击我要评论:
我的小米账号确实是已经登录的,再看一下打印信息:
这里的正常返回的状态也是1,你可能要问,这次为什么没有打印出来那个加密的小米ID?那是因为,我们在这里拦截修改了状态值,修改为2不可登录的状态,在不可登录的情况下自然是不会调用获取加密的小米ID信息!
到这里,我们本次的学习结束。其实针对HTML5的破解方法无非就是从那个关键性的 @JavascriptInterface注解下手,这也是HTML5开发App的特点所在!
根据本次的教案对象:小米应用商店,博主点评它的安全防范那就是:采用HTML5的开发模式,把关键的控件和逻辑搬到了后台服务器上,通过js与Java的交互来获取本地数据,以此来提高了整体的安全性和破解的困难性。缺点就是首先就是反应速度方面,因为走的是网页,网页的加载显然没有Android原生的界面加载迅速,再者就是存在一定的局限性,如果用户面临无网络的情况下,将无法使用该应用。总体看来说就是牺牲一些用户体验来提高应用的安全,从某种程度来说也是可取的方案。
好了,本次讲解到此结束,有不明白的地方请评论留言,我看到后会及时回复。