工具介绍这款辅助开发工具还是挺有用的,我看应用宝的下载量有几万了,当前版本号 3.1.0 。这个应用能够开启手机上的一些开发中常用的设置,应用信息提取,以及反编译的功能。所以周末的时候研究了一下。应用宝下载地址 。其中我感觉比较有用的功能就是 查看Activity的历史,所以想研究下他的查看Activity的历史的功能是如何实现的,但是他的查看 Activity历史 的功能是收费的,那我们看一下能不能绕过收费功能。
1、绕过收费功能的基本思路这个app在我的角度看来有两个破解点:
第一种思路就是硬刚,看下图,在点击这些灰色按钮的时候,普通版这些功能是不可用的,那么本地在初始化这个界面的时候肯定有验证当前app是不是已经授权了,如果没有授权就不可用,那么我们可以反转这个验证过程理论上来说就可以实现破解功能的使用了了(并且这个app不需要登陆,那么验证过程肯定也不会太难。)我大概看了下代码,这个界面是一个RecyclerView,在初始化的时候会根据授权状态实例化不同的数据对象,但是这个玩意混淆的有点烦,没再继续往下跟踪。
第二种破解方案,看下图,思考一下,这个地方填写激活码,如果激活码正确那么就能激活,所以说这里要和服务器进行通信。然后服务器会验证激活码是不是有效,我们可以随便填一个激活码,然后抓包看一下这个激活动作和服务器通信的过程。这个网络协议一般情况下也就是两种,一种是TCP协议,一种是HTTP协议。先用Charles抓一下看看能不能抓到。
我随便填写了个 1234567890 看下抓包http的请求和返回结果:
Request: GET /common/v?c=1234567890&p=cn.trinea.android.developertools&v=310&l=zh_CN&t=1541382122260 HTTP/1.1 //-------------------------华丽分割线---------------------------// Response: { "code": 11, "message": "activationCode length is illegal" } 复制代码看下这个message 它说激活码太短,那么多长才算正常长度呢?后面我买了了个激活码,发现这个激活码长度为32个字符,正好是个md5的转成BigInteger的长度。这是后话。。。
这~~~这个授权的请求和和验证已经算是非常简单的了,请求和返回数据都是http协议,而且都是明文,而且请求体没有校验参数。这里猜测一下哈,客户端拿到这么个请求结果就能判断是不是已经激活。理论上来说激活成功和激活失败的返回的json格式是一致的,当然也不排除会有不同的情况存在。先不管,先按激活成功和失败的返回结构一样来猜测,那么一般我们用返回的code来判断。这个code一般为错误码,而且一般来说0,1,-1 是有很特殊含义的。这里我们尝试用0,1,-1 来替换上面的11 。(这里只是按照一种正向开发的方式推测)
那么怎么能让手机收到被修改后的json呢?我用的Charles的URL的映射功能 菜单栏 Tools ―>Map Local Settings ,然后添加一条映射规则,大概长这么个样子:
那个ss.json 文件里面我们放如下字符串:
{ "code": 0, "message": "activationCode length is illegal" } 复制代码其实到这里呢,这个app就已经破解完了,理论上来说付费功能就可以使用了。能这么做主要是其一本地验证太过于简单,而且验证的地方太少就一次验证。
2、修改反编译后的Smali代码绕过授权验证理论上来说所有的发起网络请求都点击激活按钮开始的,那么我们就从激活的这个按钮下手,看下激活按钮触发后都进行了什么操作,以及app是如何处理网络返回的数据的。
通过ADM 找到,这个激活按钮的 id,如下图所示:
然后去 public.xml 里面找到 name 为a1 并且 type 为id的值 然后在as 里面全局搜索下id 这个值,发现并不多,一共就是四个,如下图:
找一下规律,这个16进制的数全局一共能够搜索到四处,虽然这四个在四个不同的文件里面,但是他们对应的变量名都是一样的都是active,那么依然是全局搜索:active
然后我们就通过这个按钮id的引用锁定了这个按钮的点击事件,其实寻找点击事件的方式很多,针对从这个app的角度来说,如果你输入一个字符,会有一个toast,说“激活码长度不能小于9位”,然后可以从这个提示入手,依然能定位到 d.b.a.c.h.b.a 这个类。如果从寻找点击事件的角度来说的话,通过Xposed或者 通过Android Device Monitor 里面的 Method Profiling 方法都能获取到。
//.class public Ld/b/a/c/h/b/a; (d.b.a.c.h.b.a)类里面 public void onClick(View view) { if (this.g.a()) { int id = view.getId(); if (id == this.c.getId()) { d.b.a.c.j.a.a(this.e, "pud", true); d.b.a.c.h.a.a.a(getActivity()); } else if (id == this.b.getId()) { dismiss(); } else if (id == d.b.a.c.h.j.a.active) { //下面是核心代码 Object obj = this.a.getText().toString(); if (!TextUtils.isEmpty(obj)) { String trim = obj.trim(); if (!TextUtils.isEmpty(trim) && trim.length() >= 10) { //这里会判断输入的字符串长度 i b = b(); //关键的就是他了 if (b == null) { ad.a(this.e, c.payable_is_null, 1); //这应该是个log CrashReport.postCatchedException(new TrineaUploadException("payable is null in DonationVersionDialogFragment", new NullPointerException())); return; } b.d(trim); //调用 i 的 d方法也就是 c.b.c 类里面的d(String str)方法 dismiss(); } } ad.b(this.e, c.code_length_tip, Integer.valueOf(9)); //提示激活码不能小于9位 } } } 复制代码如果本地简单的验证成功会把字符串发往服务器,网络请求是这个类。看里面的网络成功的回调,这个回调匿名对象对应的Smali 文件为 .class Ld/b/a/c/h/a$1;
找到 Smali对应的文件,先反转这个if(找到 Smali里面的代码 把if-eqz改成if-nez),我们试试能不能破解。测试发现反转了if就能破解了、然后就没有必要再分析其他代码的含义了。重新打包签名就可以使用了。
注意:apktool重打包会失败。解决方法看后面
处理到这一步,其实已经算是破解了收费功能,但是他里面还有个激活码长度限制,那么这里购买一个真正的激活码看看,发现他是32个字符。哈哈32个字符~恩 md5。那这里其实自己随机生成一个字符串,md5一下就可以,然后把我们的代码插入到 d.b.a.c.h.a(String,String,b) 方法开始的时候,修改第二个参数。就可以了。
然后我们破解这个10的限制,基本流程如下:
这个10 (Smali 里面搜索 0xa) 有两个地方验证到了、一个是:图形化界面 d.b.a.c.h.b.a 一个是网络请求类 d.b.a.c.h.a 我们把验证长度都改成1(也就是0x1),这样只要随便输入一个字符串就能验证成功了 但是好像这样有bug,也就是本地还有地方用到了这个激活码,并且对长度有限制 那也好说,我们发送的时候,把用户输入的激活码md5一下。 然后就可以啦上面的难点是如何修改原来的代码,把拦截用户输入的激活码,修改后再发送给服务器。
我们在AndroidStudio中创建一个工具类,把包名改成上面和上面的类同一个包名
package d.b.a.c.h; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Author: liuqiang * Date: 2018-11-03 * Time: 13:42 * Description:获取一个字符串的md5的字符串的表现形式类似于: * 914674d1b303467d54c0673893a19ab3 个形式 */ public class bbch { public static String getHash(String s) { try { MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(s.getBytes("UTF-8")); byte[] md5Array = md5.digest(); //byte[]通常我们会转化为十六进制的32位长度的字符串 return new BigInteger(1, md5Array).toString(16); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { return "1234567890"; } } } 复制代码我们把这个类通过as编译成Smali文件,然后在 d/b/a/c/h/a 类的a方法的开头处引用如下代码:
#d.b.a.c.h.a 类里面的方法 .method public a(Ljava/lang/String;Ljava/lang/String;Ld/b/a/c/b;)V .locals 8 const/4 v0, 0x0 #在这里引用我们自定义的工具类 invoke-static {p2}, Ld/b/a/c/h/bbch;->getHash(Ljava/lang/String;)Ljava/lang/String; #从而修改第二个参数的值 move-result-object p2 .line 57 #.....省略其他代码 .end method 复制代码如果翻译成java代码如下图所示
然后再重新打包签名,运行app。选择 使用支付宝购买-->填写任意激活码--->点击激活按钮。就能使用啦~~~
3、 遇到的几个错误重打包失败。这个我还真的不知道啥原因,不过猜测是因为这个app的资源混淆的问题。即使 使用 apktool d -r xxx.apk 命令不反编译资源文件重打包运行之后也是出错,那么只好去用Smali/baksmali 去搞了。
具体步骤如下:
//前提条件 会 baksmali/smali 的基本操作 //用zip的方式解压apk //把 classes.dex 单独 搞出来 //然后运行 baksmali d classes.dex -o out //然后classes.dex 会被反编译成 Smali文件存放到 -o 指定的目录 //修改 out 中的对应文件 //然后把Smali编译成dex 运行: smali a out/ -o ./out/classes.dex //然后把out 文件夹中的 classes.dex 覆盖上面解压的apk中的dex //然后删除原来的签名文件 //然后用zip压缩这些文件,重命名apk //然后签名这个apk 复制代码 4、学到了一个混淆操作我们知道一般情况下Activity的子类是不能混淆的,但是呢这句话说的不完全。确切的说应该是在Manifest文件中注册的Activity是不能混淆的。因为Manifest文件中要写一个Activity的class的路径的字符串。如果原始的类被混淆了,而字符串没有修改,那么Android系统在做安全验证的时候就会找不到Activity,那么就没办法通过安全验证。但是设想一下如果我们的继承关系是这个样子的:
MainActivity--->BaseActivity ---->Activity 复制代码那么试问,这个BaseActivity是不是可以混淆,经过验证这个是可以混淆的。因为BaseActivity 不需要在Manifest文件中注册,而MainActivity需要。那么我们变通一下。把这个继承链加长
a--->MainActivity--->BaseActivity ---->Activity 复制代码这个时候,a 类的实现是这个样子的:
public class a extends MainActivity { } 复制代码 其实a里面啥也没有,就是个占位符,这个a并不是混淆生成的,而是我们本来就把这个类命名为a。并且在Manifest文件里面我们就注册这个a类。那么试问这个时候,MainAc