初亦泽 发表于 2020-9-7 15:20

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

本帖最后由 初亦泽 于 2020-9-7 15:23 编辑

安卓逆向-豆瓣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一起下发了,造成了很大的资源浪费,对我们解析数据也带来了不必要的麻烦。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907123620304.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907124022466.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

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

# 2、工具准备

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

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

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

# 3、Fildder抓包


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

## 3.1 配置fildder代{过}{滤}理
打开Fildder的option 选项后,在HTTPS选项中选上解密https的选项

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907125850543.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
在connection这里把端口号改成你想要的端口号,这里我设置的8887(随便设置,不超过65535,不与其他程序占用的端口冲突即可)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907125712445.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

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

```html
http://192.168.1.102:8887
```

到这里,fildder的设置就完成了
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907130126430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

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

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/2020090713060026.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907130748766.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

## 3.3 为安卓模拟器安装证书

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

```shell
http://192.168.1.102:8887/
```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907131154609.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
然后点击最下面的蓝色超链接,这样就可以下载fildder证书到本地,下载完成后,在系统的状态栏就会有提示信息

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907131348992.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
我们单击打开FiddlerRoot.crt(因为我之前下载了多份,所以浏览器自己给加了个编号)。点击之后,可能会让你设置pin码,你自己设置一个数字密码即可。
之后,会弹出下面窗口,证书名称可以随意填,点击确定之后,证书就安装完成了,就可以抓取app的https请求了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907131546215.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

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

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


![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907132135264.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
在fildder这里,我们可以看到具体的请求

```html
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中没用的信息,我们可以得到

```html
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是追踪用户信息的一个标志,在这里并不需要。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907132550308.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
上面所说的_sig就是我们需要攻克的签名。

# 5、反汇编豆瓣APP

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

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907133725352.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
# 6、定位签名计算位置
点击搜索图标,我们搜索一下在上一节找的_sig是在哪里组装的

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907133924682.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
双击进去,可以看到一个叫做ApiSignatureHelper.a的方法获得了_sig的值

```java
Pair<String, String> a2 = ApiSignatureHelper.a(request);
```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907134026501.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
再点进去看看,可以看到这个类的实现非常简单,Pair是安卓里面的一个只有两个值的数据结构,ApiSignatureHelper.a的作用就是计算_sig的值。

```java

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()作为加密内容进行的加密。
```java
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的来源:

```java
String str4 = FrodoApi.a().e.b;
```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907135128869.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
可以清晰地看到,这个值是ZenoConfig在构造函数初始化时候传入的,是第三个参数。
我们再追究下哪里调用了这个构造函数。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907135326387.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
可以看到,这个值是从这里来的

```java
String d2 = FrodoUtils.d();
```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907135358480.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

我们再追踪一下

![在这里插入图片描述](https://img-blog.csdnimg.cn/2020090713563857.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
在这里,我们终于到了计算值密钥的位置。

```java

    @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.toByteArray(), 0);
                b = AES.a(b, encodeToString);
                c = AES.a(c, encodeToString);
            } catch (PackageManager.NameNotFoundException e2) {
                e2.printStackTrace();
            }
      }
    }
```
在这段代码中,将c作为密文

```java
c = "bHUvfbiVZUmm2sQRKwiAcw=="
```

将apk签名作为密钥,经过AES加密得到最终的加密密钥,作为前面提到的HMAC Hash算法中的加密密钥。
```java
String encodeToString = Base64.encodeToString(AppContext.a().getPackageManager().getPackageInfo(AppContext.a().getPackageName(), 64).signatures.toByteArray(), 0);
```


# 7、获取豆瓣APP的签名

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

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

```java
AppContext.a().getPackageManager().getPackageInfo(AppContext.a().getPackageName(), 64).signatures.toByteArray()
```

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

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

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

```java
    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反编译后的代码,移植到这里,计算出这个加密密钥,以后就不需要再重复计算了,最后,我们得到的结果是

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907141710530.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)
# 8、HMAC Hash加密逻辑分析

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

```java
    //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代替

```java
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 & 0xf0) >> 4]);
                  encoded.append(HEX_DIGITS & 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里面看到)

```java
api-client/1 com.douban.frodo/6.42.2(194) Android/22 product/shamu vendor/OPPO model/OPPO R11 Plusrom/androidnetwork/wifiplatform/mobile nd/1
```

关于完整工程:

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200907145006283.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIzNTk0Nzk5,size_16,color_FFFFFF,t_70#pic_center)

API示例

```java
https://frodo.douban.com/api/v2/elessar/subject/27260217/photos?count=100&apikey=0dad551ec0f84ed02907ff5c42e8ec70&_sig=gfTX2YbSiADYaG%2FzXJ%2BpNo5IhbI%3D&_ts=1599316450
```

UA

```java
api-client/1 com.douban.frodo/6.42.2(194) Android/22 product/shamu vendor/OPPO model/OPPO R11 Plusrom/androidnetwork/wifiplatform/mobile nd/1

```

必须要保证签名和UA正确,才能得到预期响应



Frank丶 发表于 2020-9-7 15:30

大佬问下 一般的APK 都能反编译?
还有就是jadx中文版给个链接 呗 麻烦了 给你加分了

tangxu1995 发表于 2020-12-4 13:21

xyelyleh 发表于 2020-10-9 10:37
分析的很详细,要是我直接hook 得到bf7dddc7c9cfe6f7 完事。哈哈

+1 hook最省事儿,不过楼主分析过程很不错

uav 发表于 2021-1-24 16:56

1024
禁止使用代{过}{滤}理
文件加壳,不能反编译
这两个有教程吗

xyelyleh 发表于 2020-10-9 10:37

分析的很详细,要是我直接hook 得到bf7dddc7c9cfe6f7 完事。哈哈

litianping 发表于 2020-9-7 15:29

666666666666,前排学习了

初亦泽 发表于 2020-9-7 15:34

Frank丶 发表于 2020-9-7 15:30
大佬问下 一般的APK 都能反编译?
还有就是jadx中文版给个链接 呗 麻烦了 给你加分了

jadx在爱盘里面有:https://down.52pojie.cn/Tools/Android_Tools/jadx-1.1.0.zip

除了加壳的都能反编译

isroot 发表于 2020-9-7 15:52

学习学习{:17_1068:}

AyangLe 发表于 2020-9-7 15:53

感谢分享,学到了。

为你一人 发表于 2020-9-7 16:00

学习了。{:1_893:}{:1_893:}

天空宫阙 发表于 2020-9-7 16:27

膜拜大佬,层层递进,思路清晰。

_知鱼之乐 发表于 2020-9-7 16:30

感谢大佬

天空宫阙 发表于 2020-9-7 16:40

本帖最后由 天空宫阙 于 2020-9-7 16:43 编辑

Frida 可以hook到第六步中str4的正确值吗?
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 豆瓣app 网络请求签名算法分析与解密