豆瓣app网络请求签名算法的分析与解密

栏目:技术教程 发布时间 2020-10-18 人气 

来源:https://www.y4f.net/206590.html

TAG:无

侵权:admin@heimacode.com

免责声明:本文图片引用自网络,如有侵权请联系我们予以删除

黑码网发布此文仅为传递信息,不代表黑码网认同其观点。

简介:安卓逆向-豆瓣app签名算法分析与解密1、背景介绍2、工具准备3、Fildder抓包3.1 配置fildder代{过}{滤}理3.2 配置安卓模拟器的代{过}{滤}理3.3 为安卓模拟器安装证书4、抓取豆瓣APP的网络请求5、反汇编豆瓣APP6、定位签名计算位置7、获取豆瓣APP的签名8、HMAC Hash加密逻辑分析9、代码实现10、注意事项完整工程上传到了GitHub上,仅限于研究使用,如不能...

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


1、背景介绍
2、工具准备
3、Fildder抓包
3.1 配置fildder代{过}{滤}理
3.2 配置安卓模拟器的代{过}{滤}理
3.3 为安卓模拟器安装证书
4、抓取豆瓣APP的网络请求
5、反汇编豆瓣APP
6、定位签名计算位置
7、获取豆瓣APP的签名
8、HMAC Hash加密逻辑分析
9、代码实现
10、注意事项


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

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


1、背景介绍

豆瓣上有很多精品的图片资源,但是豆瓣的网页端写的不咋地,在下发图片链接直接随着html一起下发了,造成了很大的资源浪费,对我们解析数据也带来了不必要的麻烦。

好的解决方式是数据通过json下发,豆瓣的移动端app就是通过下发json数据实现的通信,看到下面的图,是我抓包后得到的json数据,是不是更加清晰和好解析呢。

但是,豆瓣为了防止api被第三方使用,对api的使用做了校验。本文的目的就是逆向豆瓣app,获取豆瓣的签名算法,让我们可以自由地使用豆瓣API。

2、工具准备

本文中用到的软件和工具如下

项目 描述 链接

豆瓣app我们逆向的app各大应用市场Fildder抓包工具自行搜索jadxJava反汇编程序吾爱破解AndroidStudio编写app验证android developer夜神安卓模拟器安卓模拟器官网自行下载

注意,夜神安卓模拟器要用系统版本为5.1的,因为安卓6.0及以上版本的app不再信任我们自行设置的证书,也就没法抓到https请求

3、Fildder抓包

将必要的软件安装完成后,我们就可以使用Fildder抓包了,但是,为了抓到https请求,我们需要为夜深安卓模拟器安装证书。

3.1 配置fildder代{过}{滤}理

打开Fildder的option 选项后,在HTTPS选项中选上解密https的选项


在connection这里把端口号改成你想要的端口号,这里我设置的8887(随便设置,不超过65535,不与其他程序占用的端口冲突即可)

设置完之后,把鼠标移动到右上角的online上面,可以看到我们在局域网的ip(最下面那个ip就是),这样我们得到fildder为我们建立的代{过}{滤}理服务器的地址就是:

http://192.168.1.102:8887

到这里,fildder的设置就完成了

3.2 配置安卓模拟器的代{过}{滤}理

打开夜神安卓模拟器,找到设置-WLAN。鼠标长按WiredSSID就会出现下图中的选项

我们点击修改网络,选择高级选项,把代{过}{滤}理选到手动模式,再把我们刚刚获得的fildder代{过}{滤}理服务器地址写入,然后保存。

3.3 为安卓模拟器安装证书

我们打开安卓模拟器的浏览器,在地址栏输入我们之前得到的fildder的ip地址

http://192.168.1.102:8887/


然后点击最下面的蓝色超链接,这样就可以下载fildder证书到本地,下载完成后,在系统的状态栏就会有提示信息


我们单击打开FiddlerRoot.crt(因为我之前下载了多份,所以浏览器自己给加了个编号)。点击之后,可能会让你设置pin码,你自己设置一个数字密码即可。
之后,会弹出下面窗口,证书名称可以随意填,点击确定之后,证书就安装完成了,就可以抓取app的https请求了。

4、抓取豆瓣APP的网络请求

我们在安卓模拟器上打开豆瓣app,搜一下我们想要下载的照片


在fildder这里,我们可以看到具体的请求

https://frodo.douban.com/api/v2/elessar/subject/27260217/photos?count=100&os_rom=android&apikey=0dad551ec0f84ed02907ff5c42e8ec70&channel=Yingyongbao_Market&udid=5165b4781e5830a4f29cb0acc89e8553b1e960cd&_sig=MpYKtfAAfO8mabwd5Qa684EvidQ%3D&_ts=1599455970

先过滤掉url中没用的信息,我们可以得到

https://frodo.douban.com/api/v2/elessar/subject/27260217/photos?count=100&apikey=0dad551ec0f84ed02907ff5c42e8ec70&_sig=MpYKtfAAfO8mabwd5Qa684EvidQ%3D&_ts=1599455970

选fildder的webform选项可以看得更清楚一些。里面的apikey是固定的,_sig是加密的验证签名,_ts是以秒为单位的请求时间,和加密有关,count则是请求的图片数量(设置一百也只能请求50个,不过,可以在链接中再加入一个参数start,带start的链接需要你滚动下鼠标,加载图片,然后在fildder里面就能看到了),dfid是追踪用户信息的一个标志,在这里并不需要。

上面所说的_sig就是我们需要攻克的签名。

5、反汇编豆瓣APP

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

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

6、定位签名计算位置

点击搜索图标,我们搜索一下在上一节找的_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);

7、获取豆瓣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算法需要的加密密钥

8、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));    }

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

9、代码实现

在上面分析的代码中,有一些是安卓特有的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;    }}

10、注意事项

豆瓣为了防止抓包,还对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导入即可,测试部分在图示位置

新旗舰 数字键盘 磁化 流式 自学 过电压 工艺 科勒 西顿 胰岛素 西人 时代 中行 马达 南向 双轨 免费 我对 葫芦 说不出 缠绕 参考价格 走漏 垃圾焚烧 招式 缩短 找到 电子电器 肌瘤 热敏电阻 出错 粘贴 铭瑄 基金会 丫丫 捆扎 家具市场 李某 反垄断 管辖权 傻瓜式 电感应 产区 间歇泉 招聘网 疏散 门生 地区 生产力 全球第一款 太君 微量 又要 储藏 浙江省 软着陆 新产品 共享资源 球网 供气 覆盖 很有可能 安全带 引以 效能 饭店 党性 金水 下载游戏 排风 海燕 深海 截止阀 帐单 电击 你真好 道尔 碎片 热泵 进行分类 双倍 党组织 小猫 电源线 帮帮我 引进 可信度 离子 指标体系 汶上县 你让 官方网 预备 荣誉 持仓 传家宝 烟尘 我能 稀有 注释
资源来源网络,若未解决请查看原文

本文地址:https://www.heimacode.com/article/61116.html