逆向一款收费版的开发工具

0、工具介绍

工具的功能展现图

工具介绍这款辅助开发工具仍是挺有用的,我看应用宝的下载量有几万了,当前版本号 3.1.0 。这个应用可以开启手机上的一些开发中经常使用的设置,应用信息提取,以及反编译的功能。因此周末的时候研究了一下。应用宝下载地址 。其中我感受比较有用的功能就是 查看Activity的历史,因此想研究下他的查看Activity的历史的功能是如何实现的,可是他的查看Activity历史的功能是收费的,那咱们看一下能不能绕过收费功能。java

一、绕过收费功能的基本思路

这个app在个人角度看来有两个破解点:android

第一种思路就是硬刚,看下图,在点击这些灰色按钮的时候,普通版这些功能是不可用的,那么本地在初始化这个界面的时候确定有验证当前app是否是已经受权了,若是没有受权就不可用,那么咱们能够反转这个验证过程理论上来讲就能够实现破解功能的使用了了(而且这个app不须要登录,那么验证过程确定也不会太难。)我大概看了下代码,这个界面是一个RecyclerView,在初始化的时候会根据受权状态实例化不一样的数据对象,可是这个玩意混淆的有点烦,没再继续往下跟踪。json

安全

image.png

第二种破解方案,看下图,思考一下,这个地方填写激活码,若是激活码正确那么就能激活,因此说这里要和服务器进行通讯。而后服务器会验证激活码是否是有效,咱们能够随便填一个激活码,而后抓包看一下这个激活动做和服务器通讯的过程。这个网络协议通常状况下也就是两种,一种是TCP协议,一种是HTTP协议。先用Charles抓一下看看能不能抓到。 bash

image.png

我随便填写了个 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 。(这里只是按照一种正向开发的方式推测)app

那么怎么能让手机收到被修改后的json呢?我用的Charles的URL的映射功能 菜单栏Tools —>Map Local Settings,而后添加一条映射规则,大概长这么个样子: 工具

image.png

那个ss.json 文件里面咱们放以下字符串:post

{
	"code": 0,
	"message": "activationCode length is illegal"
}
复制代码

其实到这里呢,这个app就已经破解完了,理论上来讲付费功能就可使用了。能这么作主要是其一本地验证太过于简单,并且验证的地方太少就一次验证。

二、修改反编译后的Smali代码绕过受权验证

理论上来讲全部的发起网络请求都点击激活按钮开始的,那么咱们就从激活的这个按钮下手,看下激活按钮触发后都进行了什么操做,以及app是如何处理网络返回的数据的。

经过ADM 找到,这个激活按钮的 id,以下图所示:

image.png

而后去 public.xml 里面找到 name 为a1 而且 type 为id的值 而后在as 里面全局搜索下id 这个值,发现并很少,一共就是四个,以下图:

AS中全局搜索 0x7f09001b 的结果

找一下规律,这个16进制的数全局一共可以搜索到四处,虽然这四个在四个不一样的文件里面,可是他们对应的变量名都是同样的都是active,那么依然是全局搜索:active

Smali中全局搜索 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位
            }
        }
    }
复制代码

c.b.c 类里面的d(String str)方法

若是本地简单的验证成功会把字符串发往服务器,网络请求是这个类。看里面的网络成功的回调,这个回调匿名对象对应的Smali 文件为.class Ld/b/a/c/h/a$1;

网络请求及其返回

找到 Smali对应的文件,先反转这个if(找到 Smali里面的代码 把if-eqz改为if-nez),咱们试试能不能破解。测试发现反转了if就能破解了、而后就没有必要再分析其余代码的含义了。从新打包签名就可使用了。

注意:apktool重打包会失败。解决方法看后面

d.b.a.c.h.a$1.smali

处理到这一步,其实已经算是破解了收费功能,可是他里面还有个激活码长度限制,那么这里购买一个真正的激活码看看,发现他是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。选择 使用支付宝购买-->填写任意激活码--->点击激活按钮。就能使用啦~~~

三、 遇到的几个错误

重打包失败。这个我还真的不知道啥缘由,不过猜想是由于这个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
复制代码

四、学到了一个混淆操做

咱们知道通常状况下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类。那么试问这个时候,MainActivity 和 BaseActivity是否是就能够参与混淆。而这个时候咱们的全部业务依然在MainActivity里面实现,a仅仅是个看起来像是混淆名字的占位符而已。。。。

五、总结一下

其实这个app去除受权码校验很简单,经过Charles代理就能够解决,可是这个app仍是有不少能够借鉴的地方的,看下知识点:

  • 网络抓包的工具的使用
  • Charles 映射修改网络返回的数据
  • 逆向过程当中如何定位点击事件的实现类
  • Android Device Monitor 的使用
  • AndroidStudio全局搜索功能
  • baksmali/smali 的使用,和重打包的方式
  • 这个应用的Activity“混淆了”,这里所谓的混淆是一种伪的混淆
  • 如何在原来的代码中插入本身的代码

而后就能够反编译代码学习他是如何记录Activity的历史的了。

最后

本文的目的只有一个就是学习逆向分析技巧关注Android产品的安全。从分析上来看,混淆仍是颇有必要的,好像这个应用里面还作了资源混淆。可是这个app没有作签名验证,没有作网络加密,http协议用的明文,请求参数没有作校验,应用没有加固。若是咱们想要更好的防御本身的app能够从这几方面下手。若是有人利用本文技术进行非法操做带来的后果都是操做者本身承担,和本文以及本文做者没有任何关系。开发不易若是感受工具好用,请联系做者购买。

相关文章
相关标签/搜索