吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3785|回复: 25
收起左侧

[Android 原创] 某听书请求签名分析

  [复制链接]
天空宫阙 发表于 2022-6-3 16:25
本帖最后由 天空宫阙 于 2022-6-4 22:06 编辑

某听书sc参数

sc参数用于对请求签名验证  

使用的工具

工具名 链接
jadx 用于反编译apk为java代码 https://github.com/skylot/jadx/releases/
逍遥模拟器 (初学安卓逆向先用模拟器,真机更好) https://www.memuplay.com/tw/
Frida 用于hook https://github.com/frida/frida
Charles 或Fiddler 用于抓包

app下载地址6.6.3 https://wws.lanzouj.com/izQVQ05wmu8d

抓包数据

image-20220603160557904.png

目标是有声书的播放地址,所以是下面这个请求

GET /yyting/gateway/entityPath.action?entityId=46122241&entityType=3&opType=1§ions=%5B38%5D&type=0&token=HCvrt6XKdUjP1SZKF6GjFg**_5Q-vbHrlEjiKL1oDFkTxMA**&imei=bHJpZGEzRWxwMWU1TUw5M3F4bURCcFlibURnPT0%3D&nwt=1&q=377&mode=0&sc=c0ba56d16a055eeb60c166a32238c1f0 HTTP/1.1
User-Agent: Android7.1.2/yyting/OPPO/PCRT00/ch_qutoutiao/214/AndroidHD
Referer: yytingting.com
Accept-Encoding: gzip,deflate,sdch
ClientVersion: 6.6.3
Host: dapis.mting.info
Connection: Keep-Alive

经过多次抓包测试
entityId 是有声书的id
sections 有声书章节
token 登录的凭证(可以固定)
sc 其他参数变动时变动(后面知道是签名)
其他的可以固定

正向寻找

先使用jadx把apk反编译成java代码
可以尝试搜"sc",内容很多

private HttpUrl.Builder a(tingshu.bubei.a.c cVar, HttpUrl httpUrl) {
        HttpUrl build = httpUrl.newBuilder().addQueryParameter(Constants.KEY_IMEI, cVar.c()).addQueryParameter("nwt", cVar.d()).addQueryParameter(WidgetRequestParam.REQ_PARAM_COMMENT_TOPIC, String.valueOf(cVar.e())).addQueryParameter(Constants.KEY_MODE, String.valueOf(cVar.f())).build().newBuilder().removeAllQueryParameters("sc").build();
        return build.newBuilder().addQueryParameter("sc", cVar.a(build));
    }

最后可以找到下面这段cVar.a是一个okhttp的拦截器点进去是一个接口,线索到这里就断了

如果根据sc的长度猜测md5,可以直接搜java.security.MessageDigestmd5
可以找到

package tingshu.bubei.a;

import java.security.MessageDigest;

/* compiled from: MD5.java */
/* loaded from: classes4.dex */
public class d {
    public static String a(String str) {
        if (str == null) {
            return "";
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.reset();
            messageDigest.update(str.getBytes("UTF-8"));
            byte[] digest = messageDigest.digest();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < digest.length; i++) {
                if (Integer.toHexString(digest[i] & 255).length() == 1) {
                    sb.append("0");
                    sb.append(Integer.toHexString(digest[i] & 255));
                } else {
                    sb.append(Integer.toHexString(digest[i] & 255));
                }
            }
            return sb.toString().toLowerCase();
        } catch (Exception unused) {
            return "";
        }
    }
}

然后查找用例可以找到

package tingshu.bubei.a;

import android.net.Uri;
import java.util.Set;
import java.util.TreeMap;
import okhttp3.HttpUrl;

/* compiled from: ParamScGenerator.java */
/* loaded from: classes4.dex */
public class f {
    public static String a(HttpUrl httpUrl) {
        if (httpUrl.querySize() > 0) {
            HttpUrl b = b(httpUrl);
            // 核心md5位置
            return d.a((b.uri().getPath() + "?" + Uri.decode(b.uri().getRawQuery())) + "iJ0DgxmdC83#I&j@iwg");
        }
        // 核心md5位置
        return d.a(httpUrl.uri().getPath() + "iJ0DgxmdC83#I&j@iwg");
    }

    private static HttpUrl b(HttpUrl httpUrl) {
        Set<String> queryParameterNames = httpUrl.queryParameterNames();
        TreeMap treeMap = new TreeMap();
        HttpUrl.Builder newBuilder = httpUrl.newBuilder();
        for (String str : queryParameterNames) {
            newBuilder.removeAllQueryParameters(str);
            treeMap.put(str, httpUrl.queryParameter(str));
        }
        for (String str2 : treeMap.keySet()) {
            newBuilder.addQueryParameter(str2, (String) treeMap.get(str2));
        }
        return newBuilder.build();
    }
}

d.a((b.uri().getPath() + "?" + Uri.decode(b.uri().getRawQuery())) + "iJ0DgxmdC83#I&j@iwg");  

其中d.a就是md5,然后我们用frida来hook一下d.a,打印参数和返回值。

参数盲猜是请求url加上参数,但是我们不知道参数的拼接顺序,hook一下比较方便,当然可以研究一下下面两个,但是好像还是不知道参数的顺序,还是hook一下最方便。

public String getPath()
Returns the decoded path component of this URI.
The string returned by this method is equal to that returned by the getRawPath method except that all sequences of escaped octets are decoded.

Returns:
The decoded path component of this URI, or null if the path is undefined

public String getRawQuery()
Returns the raw query component of this URI.
The query component of a URI, if defined, only contains legal URI characters.

Returns:
The raw query component of this URI, or null if the query is undefined

hook验证

hook代码

import frida, sys

def on_message(message, data):
    if message['type'] == 'send':
        print(" {0}".format(message['payload']))
    else:
        print(message)

jscode = """
Java.perform(function () {
  let d = Java.use("tingshu.bubei.a.d");
  d["a"].implementation = function (str) {
      console.log('a is called' + ', ' + 'str: ' + str);
      let ret = this.a(str);
      console.log('a ret value is ' + ret);
      return ret;
};
});
"""

device = frida.get_remote_device()
process = device.attach("懒人听书")
script = process.create_script(jscode)
script.on('message', on_message)
print(' Running Hook')
script.load()
sys.stdin.read()

hook结果

a is called, str: /yyting/gateway/entityPath.action?entityId=46122241&entityType=3&imei=bHJpZGEzRWxwMWU1TUw5M3F4bURCcFlibURnPT0=&mode=0&nwt=1&opType=1&q=379§ions=[40]&token=HCvrt6XKdUjP1SZKF6GjFg**_5Q-vbHrlEjiKL1oDFkTxMA**&type=0iJ0DgxmdC83#I&j@iwg
a ret value is eeb445260d0fdde00d900c24d0f10256

验证了sc通过md5加密得到加密前的字符串/yyting/gateway/entityPath.action?entityId=46122241&entityType=3&imei=bHJpZGEzRWxwMWU1TUw5M3F4bURCcFlibURnPT0=&mode=0&nwt=1&opType=1&q=379§ions=[40]&token=HCvrt6XKdUjP1SZKF6GjFg**_5Q-vbHrlEjiKL1oDFkTxMA**&type=0iJ0DgxmdC83#I&j@iwg

python模拟请求

import requests
import hashlib

def get_paly_url(entityId, sections):
    token = 'HCvrt6XKdUjP1SZKF6GjFg**_5Q-vbHrlEjiKL1oDFkTxMA**'
    sc_raw = f'/yyting/gateway/entityPath.action?entityId={str(entityId)}&entityType=3&imei=bHJpZGEzRWxwMWU1TUw5M3F4bURCcFlibURnPT0=&mode=0&nwt=1&opType=1&q=377§ions=[{str(sections)}]&token={token}&type=0iJ0DgxmdC83#I&j@iwg'
    sc = hashlib.md5(sc_raw.encode()).digest().hex()
    url = f"https://dapis.mting.info/yyting/gateway/entityPath.action?entityId={str(entityId)}&entityType=3&opType=1§ions=[{str(sections)}]&type=0&token={token}&imei=bHJpZGEzRWxwMWU1TUw5M3F4bURCcFlibURnPT0%3D&nwt=1&q=377&mode=0&sc={sc}"
    payload = {}
    headers = {
        'User-Agent': 'Android7.1.2/yyting/OPPO/PCRT00/ch_qutoutiao/214/AndroidHD',
        'Referer': 'yytingting.com',
        'ClientVersion': '6.6.3',
        'Host': 'dapis.mting.info'
    }
    response = requests.request(
        "GET", url, headers=headers, data=payload, verify=False)
    print(response.text)

if __name__ == "__main__":
    entityId = 46122241
    for sections in range(1,20):
        get_paly_url(entityId,sections)

成功留念

image-20220603161221768.png

其他经验总结

  • 可以一开始就hook md5就可以秒杀了

    
    Java.perform(function() {
    // MD SHA 
    var messageDigest=Java.use('java.security.MessageDigest');
    // update
    for(var i = 0; i < messageDigest.update.overloads.length; i++){
        messageDigest.update.overloads[i].implementation = function(){
            var name=this.getAlgorithm()
            send("================="+name+"====================");
            send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
    
            for (var i = 0; i < arguments.length; i++) {
                send('messageDigest.update')
                send('JSON.stringify:' + JSON.stringify(arguments[i]))
                send('str:'+Java.use('java.lang.String').$new(arguments[i]))
            }
            var ret = this.update.apply(this, arguments)
        }
    }
    // digest
    for(var i = 0; i < messageDigest.digest.overloads.length; i++){
        messageDigest.digest.overloads[i].implementation = function(){
            var name=this.getAlgorithm()
            send("================="+name+"====================");
            send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            for (var i = 0; i < arguments.length; i++) {
                send('messageDigest.digestarguments:' + JSON.stringify(arguments[i]))
            }
            var ret = this.digest.apply(this, arguments)
            send('return:' + JSON.stringify(ret))
            send('str:'+Java.use('java.lang.String').$new(ret))
            // printInfo(ret)
            return ret
        }
    }

});



- 可以同时hook Base64 AES RSA SHA1 SHA256等常见编码或加密就更为保险
- jadx支持直接复制为frida片段

免费评分

参与人数 10威望 +1 吾爱币 +30 热心值 +9 收起 理由
0620zhao + 1 + 1 我很赞同!
hanlaoshi + 1 + 1 谢谢@Thanks!
zsz868 + 1 我很赞同!
独行风云 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
bzfc + 1 + 1 nb
leannie + 1 + 1 谢谢@Thanks!
gaowudao + 1 + 1 感谢
笙若 + 1 + 1 谢谢@Thanks!
正己 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
我是不会改名的 + 2 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

book96 发表于 2022-6-6 11:16
java层的常用算法,直接用算法大师能hook出来吧
nanfeng123 发表于 2022-6-4 16:34
用jadx找到sc参数的位置和大佬的不一样
[Java] 纯文本查看 复制代码
package l.a.a.l;

import androidx.annotation.NonNull;
import com.taobao.accs.common.Constants;
import java.io.IOException;
import java.util.Set;
import l.a.a.d;
import l.a.a.f;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/* compiled from: DefaultParamInterceptor.java */
/* loaded from: classes.dex */
public class c implements Interceptor {
    @NonNull
    private HttpUrl.Builder a(d dVar, HttpUrl httpUrl) {
        HttpUrl build = httpUrl.newBuilder().addQueryParameter("imei", dVar.c()).addQueryParameter("nwt", dVar.g()).addQueryParameter("q", String.valueOf(dVar.a())).addQueryParameter(Constants.KEY_MODE, String.valueOf(dVar.f())).addQueryParameter("lrid", dVar.h()).build().newBuilder().removeAllQueryParameters("sc").build();
        return build.newBuilder().addQueryParameter("sc", dVar.b(build));
    }


md5的算法
[Java] 纯文本查看 复制代码
package bubei.tingshu.lib.b.i;

import android.util.Log;
import com.google.common.primitives.UnsignedBytes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/* compiled from: MD5.java */
/* loaded from: classes3.dex */
class f {
    /* JADX INFO: Access modifiers changed from: package-private */
    public static String a(String str) {
        if (str == null) {
            return "";
        }
        MessageDigest messageDigest = null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.reset();
            messageDigest.update(str.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException unused) {
            Log.e("MD5", "NoSuchAlgorithmException caught!");
            System.exit(-1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        byte[] digest = messageDigest.digest();
        StringBuffer stringBuffer = new StringBuffer();
        for (int i2 = 0; i2 < digest.length; i2++) {
            if (Integer.toHexString(digest[i2] & UnsignedBytes.MAX_VALUE).length() == 1) {
                stringBuffer.append("0");
                stringBuffer.append(Integer.toHexString(digest[i2] & UnsignedBytes.MAX_VALUE));
            } else {
                stringBuffer.append(Integer.toHexString(digest[i2] & UnsignedBytes.MAX_VALUE));
            }
        }
        return stringBuffer.toString().toLowerCase();
    }
}


用到md5的地方
[Java] 纯文本查看 复制代码
package bubei.tingshu.lib.b.i;

import android.text.TextUtils;
import anet.channel.util.HttpConstant;
import com.qiyukf.module.log.classic.spi.CallerData;
import com.tachikoma.core.utility.UriUtil;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;

/* compiled from: HttpUtil.java */
/* loaded from: classes3.dex */
public class c {
    public static String a = "vYCmm+6CFVykQk5w0wiUDliCQRA=";

    public static String a(Map<String, String> map) {
        StringBuilder sb = new StringBuilder();
        for (String str : map.keySet()) {
            if (sb.length() != 0) {
                sb.append("&");
            }
            sb.append(str);
            sb.append("=");
            String str2 = map.get(str);
            if (str2 == null || TextUtils.isEmpty(str2) || "null".equals(str2)) {
                str2 = "";
            }
            sb.append(str2);
        }
        return sb.toString();
    }

    public static String b(String str) {
        if (str == null) {
            return "";
        }
        try {
            return URLEncoder.encode(str, "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    public static String c(Map<String, String> map) {
        StringBuilder sb = new StringBuilder();
        for (String str : map.keySet()) {
            if (sb.length() != 0) {
                sb.append("&");
            }
            sb.append(b(str));
            sb.append("=");
            sb.append(b(map.get(str)));
        }
        return sb.toString();
    }

    public static String d(String str, Map<String, String> map) {
        String str2;
        String str3;
        if (e(str)) {
            f(map);
            if (map.size() > 0) {
                String a2 = a(map);
                try {
                    URL url = new URL(str);
                    String str4 = url.getProtocol() + HttpConstant.SCHEME_SPLIT + url.getHost();
                    str3 = str.replace(str4, "") + CallerData.NA + a2;
                } catch (MalformedURLException unused) {
                    str3 = "";
                }
                map.put("sc", f.a(str3 + a));
                String c = c(map);
                if (c == null || c.equals("")) {
                    return str;
                }
                return str + CallerData.NA + c;
            }
            try {
                URL url2 = new URL(str);
                str2 = str.replace(url2.getProtocol() + HttpConstant.SCHEME_SPLIT + url2.getHost(), "");
            } catch (MalformedURLException unused2) {
                str2 = "";
            }
            map.put("sc", f.a(str2 + a));
            String c2 = c(map);
            if (c2 == null || c2.equals("")) {
                return str;
            }
            return str + CallerData.NA + c2;
        }
        return "";
    }


python代码
把let d = Java.use("tingshu.bubei.a.d");
改为了 let d = Java.use("bubei.tingshu.lib.b.i.f")

我用的版本是 7.1.9
大佬用的是6.6.3吗?

frida运行的时候app直接就停止运行了
有2个疑问:
1、我找的代码位置和大佬的不一样,是我找错了吗?
2、frida运行的时候app直接就停止运行了,请问是我的下载的版本太高了吗?大佬是如何操作的?
o824 发表于 2022-6-3 17:37
传闻中的喜哥哥 发表于 2022-6-3 17:41
完全没看懂  纯点赞
dearkk 发表于 2022-6-3 18:02
学习看看,感谢楼主
zg2600 发表于 2022-6-3 18:41
学习了 分析的到位
Βigbang 发表于 2022-6-3 23:18
大佬一看就很厉害,能不能帮帮忙https://www.52pojie.cn/thread-1644413-1-1.html
slbcmgn 发表于 2022-6-4 07:26
学习楼主的技术要领
kkk782 发表于 2022-6-4 09:57
学习看看,感谢楼主
zzz1233456 发表于 2022-6-4 15:15
完全没看懂  纯点赞, 感谢大佬
nanfeng123 发表于 2022-6-4 16:33
本帖最后由 nanfeng123 于 2022-6-4 17:23 编辑

写的不错
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 13:46

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表