安卓逆向-豆瓣app签名算法分析与解密(下)


完整工程上传到了GitHub上,仅限于研究使用,欢迎star,如不能运行请看注意事项

项目地址:https://github.com/bestyize/DoubanAPI


1、反汇编豆瓣APP

我们用强大的jadx来反汇编豆瓣app

选择文件-打开。然后找到豆瓣app的安装包后打开。

在这里插入图片描述

2、定位签名计算位置

点击搜索图标,我们搜索一下在上一节找的_sig是在哪里组装的

在这里插入图片描述
双击进去,可以看到一个叫做ApiSignatureHelper.a的方法获得了_sig的值

Pair<String, String> a2 = ApiSignatureHelper.a(request);

在这里插入图片描述
再点进去看看,可以看到这个类的实现非常简单,Pair是安卓里面的一个只有两个值的数据结构,ApiSignatureHelper.a的作用就是计算_sig的值。


public class ApiSignatureHelper {
    static Pair<String, String> a(Request request) {
        if (request == null) {
            return null;
        }
        String header = request.header(com.douban.push.internal.api.Request.HEADER_AUTHORIZATION);
        if (!TextUtils.isEmpty(header)) {
            header = header.substring(7);
        }
        return a(request.url().toString(), request.method(), header);
    }

    public static Pair<String, String> a(String str, String str2, String str3) {
        String decode;
        if (TextUtils.isEmpty(str)) {
            return null;
        }
        String str4 = FrodoApi.a().e.b;
        if (TextUtils.isEmpty(str4)) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(str2);
        String encodedPath = HttpUrl.parse(str).encodedPath();
        if (encodedPath == null || (decode = Uri.decode(encodedPath)) == null) {
            return null;
        }
        if (decode.endsWith("/")) {
            decode = decode.substring(0, decode.length() - 1);
        }
        sb.append(StringPool.AMPERSAND);
        sb.append(Uri.encode(decode));
        if (!TextUtils.isEmpty(str3)) {
            sb.append(StringPool.AMPERSAND);
            sb.append(str3);
        }
        long currentTimeMillis = System.currentTimeMillis() / 1000;
        sb.append(StringPool.AMPERSAND);
        sb.append(currentTimeMillis);
        return new Pair<>(HMACHash1.a(str4, sb.toString()), String.valueOf(currentTimeMillis));
    }
}

看完代码后我们可以知道,最后是使用了HMAC Hash算法,把str4作为key,把sb.toString()作为加密内容进行的加密。

public class HMACHash1 {
    public static final String a(String str, String str2) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(str.getBytes(), LiveHelper.HMAC_SHA1);
            Mac instance = Mac.getInstance(LiveHelper.HMAC_SHA1);
            instance.init(secretKeySpec);
            return Base64.encodeToString(instance.doFinal(str2.getBytes()), 2);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

但是由于HMAC Hash是一个不可逆的加密算法,我们是不能根据_sig来反推加密密钥的。

所以我们能做的就是直接获取这个加密密钥。

我们追一下str4的来源:

String str4 = FrodoApi.a().e.b;

在这里插入图片描述
可以清晰地看到,这个值是ZenoConfig在构造函数初始化时候传入的,是第三个参数。
我们再追究下哪里调用了这个构造函数。

在这里插入图片描述
可以看到,这个值是从这里来的

String d2 = FrodoUtils.d();

在这里插入图片描述

我们再追踪一下

在这里插入图片描述
在这里,我们终于到了计算值密钥的位置。


    @SuppressLint({"PackageManagerGetSignatures"})
    public static void a(boolean z) {
        if (TextUtils.isEmpty(b)) {
            b = "74CwfJd4+7LYgFhXi1cx0IQC35UQqYVFycCE+EVyw1E=";
        }
        if (TextUtils.isEmpty(c)) {
            c = "bHUvfbiVZUmm2sQRKwiAcw==";
        }
        if (z) {
            try {
                String encodeToString = Base64.encodeToString(AppContext.a().getPackageManager().getPackageInfo(AppContext.a().getPackageName(), 64).signatures[0].toByteArray(), 0);
                b = AES.a(b, encodeToString);
                c = AES.a(c, encodeToString);
            } catch (PackageManager.NameNotFoundException e2) {
                e2.printStackTrace();
            }
        }
    }

在这段代码中,将c作为密文

c = "bHUvfbiVZUmm2sQRKwiAcw=="

将apk签名作为密钥,经过AES加密得到最终的加密密钥,作为前面提到的HMAC Hash算法中的加密密钥。

 String encodeToString = Base64.encodeToString(AppContext.a().getPackageManager().getPackageInfo(AppContext.a().getPackageName(), 64).signatures[0].toByteArray(), 0);

3、获取豆瓣APP的签名

在上一节中,我们定位到了计算HMAC Hash算法密钥的位置,这个位置是由AES加密获取到一个结果,作为HMAC Hash算法密钥的,但是AES加密的文本我们可以直接找到,就是

bHUvfbiVZUmm2sQRKwiAcw==

但我们还不知道AES加密密钥是什么。熟悉安卓开发的人应该知道,这句话是用来获取当前应用的签名的,这是安卓的一种防篡改的安全机制。只要我们修改了包,签名就会变化,所以,我们不能直接修改豆瓣APP的安装包。

AppContext.a().getPackageManager().getPackageInfo(AppContext.a().getPackageName(), 64).signatures[0].toByteArray()

不过,其他应用也可以获取已安装应用的签名信息,只需要把对应app的包名填入即可。

Application application=(Application)getApplicationContext();
PackageInfo packageInfo=application.getPackageManager().getPackageInfo("com.douban.frodo",PackageManager.GET_SIGNATURES);
            String sign=Base64.encodeToString(packageInfo.signatures[0].toByteArray(),0);

这样我们就获取到了我们需要的字串

    public final static String SIGN="MIICUjCCAbsCBEty1MMwDQYJKoZIhvcNAQEEBQAwcDELMAkGA1UEBhMCemgxEDAOBgNVBAgTB0Jl\n" +
            "aWppbmcxEDAOBgNVBAcTB0JlaWppbmcxEzARBgNVBAoTCkRvdWJhbiBJbmMxFDASBgNVBAsTC0Rv\n" +
            "dWJhbiBJbmMuMRIwEAYDVQQDEwlCZWFyIFR1bmcwHhcNMTAwMjEwMTU0NjExWhcNMzcwNjI3MTU0\n" +
            "NjExWjBwMQswCQYDVQQGEwJ6aDEQMA4GA1UECBMHQmVpamluZzEQMA4GA1UEBxMHQmVpamluZzET\n" +
            "MBEGA1UEChMKRG91YmFuIEluYzEUMBIGA1UECxMLRG91YmFuIEluYy4xEjAQBgNVBAMTCUJlYXIg\n" +
            "VHVuZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAg622fxLuwQtC8KLYp5gHk0OmfrFiIisz\n" +
            "kzPLBhKPZDHjYS1URhQpzf00T8qg2oEwJPPELjN2Q7YOoax8UINXLhMgFQkyAvMfjdEOSfoKH93p\n" +
            "v2d4n/IjQc/TaDKu6yb53DOq76HTUYLcfLKOXaGwGjAp3QqTqP9LnjJjGZCdSvMCAwEAATANBgkq\n" +
            "hkiG9w0BAQQFAAOBgQA3MovcB3Hv4bai7OYHU+gZcGQ/8sOLAXGD/roWPX3gm9tyERpGztveH35p\n" +
            "aI3BrUWg2Vir0DRjbR48b2HxQidQTVIH/HOJHV0jgYNDviD18/cBwKuLiBvdzc2Fte+zT0nnHXMy\n" +
            "E6tVeW3UdHC1UvzyB7Qcxiu4sBiEO1koToQTWw==\n";

不过,这个加密密钥其实是固定的,我们直接把jadx反编译后的代码,移植到这里,计算出这个加密密钥,以后就不需要再重复计算了,最后,我们得到的结果是

bf7dddc7c9cfe6f7

这就是HMAC Hash算法需要的加密密钥

在这里插入图片描述

4、HMAC Hash加密逻辑分析

在得到HAMC Hash的加密密钥之后,我们再看一下,被HMAC Hash算法加密的字符串是怎么得到的。

    //str:API的地址,不包括后面参数,举例:str="https://frodo.douban.com/api/v2/elessar/subject/27260217/photos"
    //str2:请求方法,这里是GET,举例:str2="GET"
    //str3: str3=null;
    public static Pair<String, String> a(String str, String str2, String str3) {
        String decode;
        if (TextUtils.isEmpty(str)) {
            return null;
        }
        String str4 = FrodoApi.a().e.b;//HMAC Hash密钥,在前面我们得到的结果是:bf7dddc7c9cfe6f7
        if (TextUtils.isEmpty(str4)) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(str2);
        String encodedPath = HttpUrl.parse(str).encodedPath();
        if (encodedPath == null || (decode = Uri.decode(encodedPath)) == null) {
            return null;
        }
        if (decode.endsWith("/")) {
            decode = decode.substring(0, decode.length() - 1);
        }
        sb.append(StringPool.AMPERSAND);
        sb.append(Uri.encode(decode));
        if (!TextUtils.isEmpty(str3)) {
            sb.append(StringPool.AMPERSAND);
            sb.append(str3);
        }
        long currentTimeMillis = System.currentTimeMillis() / 1000;//当前时间,取秒,也被当作被加密的内容了
        sb.append(StringPool.AMPERSAND);
        sb.append(currentTimeMillis);
        return new Pair<>(HMACHash1.a(str4, sb.toString()), String.valueOf(currentTimeMillis));
    }

至此,豆瓣的加密算法分析完成,接下来就是实现它

5、代码实现

在上面分析的代码中,有一些是安卓特有的API,但是为了让程序能run everywhere,我对其中的一些数据结构做了替换,对一些API进行了移植(感谢安卓是开源的)

由于数据结构Pair仅仅是Android中的一个类,所以,为了在别的地方用Java的地方也能用,我们可以移植,也可以用hashMap代替

public class SignatureHelper {
    private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
    private static final String DEFAULT_ENCODING = "UTF-8";
    public static final String AMPERSAND = "&";
    private final static int NOT_FOUND = -1;
//    public final static String SIGN="MIICUjCCAbsCBEty1MMwDQYJKoZIhvcNAQEEBQAwcDELMAkGA1UEBhMCemgxEDAOBgNVBAgTB0Jl\n" +
//            "aWppbmcxEDAOBgNVBAcTB0JlaWppbmcxEzARBgNVBAoTCkRvdWJhbiBJbmMxFDASBgNVBAsTC0Rv\n" +
//            "dWJhbiBJbmMuMRIwEAYDVQQDEwlCZWFyIFR1bmcwHhcNMTAwMjEwMTU0NjExWhcNMzcwNjI3MTU0\n" +
//            "NjExWjBwMQswCQYDVQQGEwJ6aDEQMA4GA1UECBMHQmVpamluZzEQMA4GA1UEBxMHQmVpamluZzET\n" +
//            "MBEGA1UEChMKRG91YmFuIEluYzEUMBIGA1UECxMLRG91YmFuIEluYy4xEjAQBgNVBAMTCUJlYXIg\n" +
//            "VHVuZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAg622fxLuwQtC8KLYp5gHk0OmfrFiIisz\n" +
//            "kzPLBhKPZDHjYS1URhQpzf00T8qg2oEwJPPELjN2Q7YOoax8UINXLhMgFQkyAvMfjdEOSfoKH93p\n" +
//            "v2d4n/IjQc/TaDKu6yb53DOq76HTUYLcfLKOXaGwGjAp3QqTqP9LnjJjGZCdSvMCAwEAATANBgkq\n" +
//            "hkiG9w0BAQQFAAOBgQA3MovcB3Hv4bai7OYHU+gZcGQ/8sOLAXGD/roWPX3gm9tyERpGztveH35p\n" +
//            "aI3BrUWg2Vir0DRjbR48b2HxQidQTVIH/HOJHV0jgYNDviD18/cBwKuLiBvdzc2Fte+zT0nnHXMy\n" +
//            "E6tVeW3UdHC1UvzyB7Qcxiu4sBiEO1koToQTWw==\n";

    public static Map<String, String> getVerifyMap(String str, String str2, String str3) {
        Map<String, String> map=new HashMap<>();
        String decode;
        if (TextUtils.isEmpty(str)) {
            return null;
        }
        String str4 = "bf7dddc7c9cfe6f7";
        if (TextUtils.isEmpty(str4)) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(str2);
        String encodedPath = encodedPath(str);
        System.out.println(encodedPath);
        if (encodedPath == null || (decode = encodedPath) == null) {
            return null;
        }
        if (decode.endsWith("/")) {
            decode = decode.substring(0, decode.length() - 1);
        }
        sb.append(AMPERSAND);
        sb.append(uriEncode(decode,null));
        if (!TextUtils.isEmpty(str3)) {
            sb.append(AMPERSAND);
            sb.append(str3);
        }
        long currentTimeMillis = System.currentTimeMillis() / 1000;
        sb.append(AMPERSAND);
        sb.append(currentTimeMillis);
        try {
            map.put("_sig", URLEncoder.encode(HMACHash1.a(str4, sb.toString()),"utf-8"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        map.put("_ts",String.valueOf(currentTimeMillis));
        return map;
    }

    public static String uriEncode(String s, String allow) {
        if (s == null) {
            return null;
        }
        StringBuilder encoded = null;

        int oldLength = s.length();
        int current = 0;
        while (current < oldLength) {
            int nextToEncode = current;
            while (nextToEncode < oldLength
                    && isAllowed(s.charAt(nextToEncode), allow)) {
                nextToEncode++;
            }
            if (nextToEncode == oldLength) {
                if (current == 0) {
                    // We didn't need to encode anything!
                    return s;
                } else {
                    // Presumably, we've already done some encoding.
                    encoded.append(s, current, oldLength);
                    return encoded.toString();
                }
            }

            if (encoded == null) {
                encoded = new StringBuilder();
            }

            if (nextToEncode > current) {
                // Append allowed characters leading up to this point.
                encoded.append(s, current, nextToEncode);
            } else {
                // assert nextToEncode == current
            }

            current = nextToEncode;
            int nextAllowed = current + 1;
            while (nextAllowed < oldLength
                    && !isAllowed(s.charAt(nextAllowed), allow)) {
                nextAllowed++;
            }
            String toEncode = s.substring(current, nextAllowed);
            try {
                byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING);
                int bytesLength = bytes.length;
                for (int i = 0; i < bytesLength; i++) {
                    encoded.append('%');
                    encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]);
                    encoded.append(HEX_DIGITS[bytes[i] & 0xf]);
                }
            } catch (UnsupportedEncodingException e) {
                throw new AssertionError(e);
            }

            current = nextAllowed;
        }
        return encoded == null ? s : encoded.toString();
    }

    private static boolean isAllowed(char c, String allow) {
        return (c >= 'A' && c <= 'Z')
                || (c >= 'a' && c <= 'z')
                || (c >= '0' && c <= '9')
                || "_-!.~'()*".indexOf(c) != NOT_FOUND
                || (allow != null && allow.indexOf(c) != NOT_FOUND);
    }

    public static String encodedPath(String url) {
        String scheme="https";
        int pathStart = url.indexOf('/', scheme.length() + 3); // "://".length() == 3.
        int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#");
        return url.substring(pathStart, pathEnd);
    }

    public static int delimiterOffset(String input, int pos, int limit, String delimiters) {
        for(int i = pos; i < limit; ++i) {
            if (delimiters.indexOf(input.charAt(i)) != -1) {
                return i;
            }
        }
        return limit;
    }
}

6、注意事项

豆瓣为了防止抓包,还对UA进行了校验,在计算出正确地址后,如果想要请求API,需要把UA设置成(这里的UA也可以在fildder里面看到)

api-client/1 com.douban.frodo/6.42.2(194) Android/22 product/shamu vendor/OPPO model/OPPO R11 Plus  rom/android  network/wifi  platform/mobile nd/1

关于完整工程:

完整工程是一个servlet程序,用idea导入即可,测试部分在图示位置

在这里插入图片描述

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
某flutter-app逆向分析是指对于一个使用flutter框架开发的应用进行逆向工程分析。逆向工程是通过分析应用的代码、二进制文件等来了解其内部实现细节。 首先,我们需要获取该应用的安装包文件(APK或IPA文件),然后进行解包操作,将其转换为可读取的文件目录结构。 接下来,我们可以使用一些工具来提取应用的资源文件、代码文件等。对于flutter-app来说,可以提取出dart文件,这是flutter的主要代码文件,其中包含了应用的逻辑实现。 通过阅读dart文件,我们可以了解应用的代码结构、数据模型、界面设计等。可以分析应用的逻辑实现方法,包括各种函数、类、方法的调用关系。 同时,还可以通过分析相关配置文件、资源文件等来了解应用的各种设置、资源加载方式等。 在逆向过程中,还可以使用一些调试工具来进一步了解应用的运行机制。例如,hook工具可以拦截应用的函数调用,并捕获输入输出数据,用于进一步分析。 逆向分析的目的可以有很多,比如了解应用的工作原理、发现潜在的漏洞或安全问题、提供参考用于自己的开发等。 需要注意的是,逆向分析需要遵守法律规定。未经授权的逆向分析可能侵犯他人的知识产权,涉及到隐私等方面的问题。因此,在进行逆向分析之前,应该了解并遵守当地相关法律法规,避免产生法律纠纷。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值