某听书请求签名分析
本帖最后由 天空宫阙 于 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
## 抓包数据
目标是有声书的播放地址,所以是下面这个请求
```
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"`,内容很多
```java
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.MessageDigest`或`md5`
可以找到
```java
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 & 255).length() == 1) {
sb.append("0");
sb.append(Integer.toHexString(digest & 255));
} else {
sb.append(Integer.toHexString(digest & 255));
}
}
return sb.toString().toLowerCase();
} catch (Exception unused) {
return "";
}
}
}
```
然后查找用例可以找到
```java
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代码
```python
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=&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=&token=HCvrt6XKdUjP1SZKF6GjFg**_5Q-vbHrlEjiKL1oDFkTxMA**&type=0iJ0DgxmdC83#I&j@iwg`
## python模拟请求
```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)
```
**成功留念**
## 其他经验总结
- 可以一开始就hook md5就可以秒杀了
```javascript
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.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))
send('str:'+Java.use('java.lang.String').$new(arguments))
}
var ret = this.update.apply(this, arguments)
}
}
// digest
for(var i = 0; i < messageDigest.digest.overloads.length; i++){
messageDigest.digest.overloads.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))
}
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片段 java层的常用算法,直接用算法大师能hook出来吧 用jadx找到sc参数的位置和大佬的不一样
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的算法
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 & UnsignedBytes.MAX_VALUE).length() == 1) {
stringBuffer.append("0");
stringBuffer.append(Integer.toHexString(digest & UnsignedBytes.MAX_VALUE));
} else {
stringBuffer.append(Integer.toHexString(digest & UnsignedBytes.MAX_VALUE));
}
}
return stringBuffer.toString().toLowerCase();
}
}
用到md5的地方
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直接就停止运行了,请问是我的下载的版本太高了吗?大佬是如何操作的?
学习看看~~~~~~~ 完全没看懂纯点赞 学习看看,感谢楼主 学习了 分析的到位 大佬一看就很厉害,能不能帮帮忙https://www.52pojie.cn/thread-1644413-1-1.html 学习楼主的技术要领 学习看看,感谢楼主 完全没看懂纯点赞, 感谢大佬 本帖最后由 nanfeng123 于 2022-6-4 17:23 编辑
写的不错