Zer0e's Blog

对bsgamesdk的签名逆向分析

字数统计: 2.6k阅读时长: 12 min
2021/04/05 Share

前言

最近看到pcr查看个人信息的脚本,比较感兴趣就稍微研究了一下。之前很早的时候对pcr进行过抓包,没想到是基于http协议的,感觉日本那边的it技术比国内的稍微落后一点。
pcr接入的是bilibili游戏,要登录游戏得先登录b站账号,抓包之后发现登录过程还是比较简单的,首先是从服务器获取rsa公钥,然后把密码进行rsa加密,添加公共请求头就可以登录了。但是请求中存在的sign参数文档并没有公开。
虽然网上的脚本已经把逻辑写清楚,但是我还是决定深入sdk源码,查看签名的逻辑。

正文

准备工作

官网上下载最新的SDK和demo,用AndroidStudio打开工程就可以准备开始了。

分析过程

在左侧的external libraries点开demo项目所依赖的sdk。有classes.jar和r-classes.jar。r里面就只有与Alipay相关的东西,应该不是所要找的代码。
点开classes.jar,随便点开几个发现都被简单混淆了。随后翻到

1
classes.jar!\com\bsgamesdk\android\api

这个包应该是与api请求相关的。
大部分类都被混淆了。只留下三个有名字的类

1
2
3
BSGameSdkAuth
BSGameSdkExceptionCode
BSGameSdkHttpQueryMap

一个一个看,Auth类基本上都是parse方法,对传入的参数进行json解析。ExceptionCode则是各类错误码和错误提示。
那重点可能会在HttpQueryMap中。

初露曙光

1
2
3
4
5
6
7
8
9
10
private String a(String var1) {
var1 = var1.replace("+", "%20");
var1 = var1.replace("*", "%2A");
var1 = var1.replace("%7E", "~");
var1 = var1.replace("!", "%21");
var1 = var1.replace("(", "%28");
var1 = var1.replace(")", "%29");
var1 = var1.replace("'", "%27");
return var1;
}

首先是个转义方法,pass。接着看下一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void appendToUri(Builder var1, String var2) throws UnsupportedEncodingException, NoSuchAlgorithmException {
BSGameSdkHttpQueryMap.a var3 = this.getSignedQuery(var2);
Iterator var4 = this.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
String var7 = (String)var5.getValue();
var1.appendQueryParameter(var6, var7);
}

var1.appendQueryParameter("sign", var3.b);
String var9 = var1.build().getEncodedQuery();
String var8 = this.a(var9);
var1.encodedQuery(var8);
}

我们看到了sign,就是我们想要知道的,也就是sign的值应该是var3.b,而var3则是从getSignedQuery方法获得的。并且是一个BSGameSdkHttpQueryMap.a类,这是一个内部类

1
2
3
4
5
6
7
private static class a {
String a;
String b;

private a() {
}
}

接着看getSignedQuery方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public BSGameSdkHttpQueryMap.a getSignedQuery(String var1) throws UnsupportedEncodingException, NoSuchAlgorithmException {
BSGameSdkHttpQueryMap.a var2 = new BSGameSdkHttpQueryMap.a();
StringBuilder var3 = new StringBuilder();
Iterator var4 = this.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
String var7 = (String)var5.getValue();
String var8 = URLEncoder.encode(var7, "UTF-8");
var8 = this.a(var8);
var3.append(var6.toLowerCase(Locale.US));
var3.append('=').append(var8).append('&');
}

int var13 = var3.length();
if (var13 > 0) {
var3.deleteCharAt(var13 - 1);
}

var2.a = var3.toString();
MessageDigest var14 = MessageDigest.getInstance("MD5");
var14.reset();
var14.update(var2.a.getBytes("UTF-8"));
var14.update(var1.getBytes("UTF-8"));
StringBuffer var15 = new StringBuffer();
byte[] var16 = var14.digest();
byte[] var17 = var16;
int var9 = var16.length;

for(int var10 = 0; var10 < var9; ++var10) {
byte var11 = var17[var10];
int var12 = var11 & 255;
if (var12 < 16) {
var15.append('0');
}

var15.append(Integer.toHexString(var12));
}

var2.b = var15.toString().toLowerCase(Locale.US);
return var2;
}

先看前一部分,首先是对var4进行迭代,把所有的参数和值以key=value&的方式进行拼接,然后去掉最后一个&符号。
接着是调用MessageDigest对所有参数进行md5,但是注意

1
2
var14.update(var2.a.getBytes("UTF-8"));
var14.update(var1.getBytes("UTF-8"));

var14除了对参数进行update外还对var1进行了update。值得一提的是,messagedigest进行多次update相当于两个拼接后再进行update。因此这个var1是关键,而var1又是appendToUri的var2,因此只需要寻找appendToUri的调用者就知道这个参数是什么了。
但是遗憾的是,ide对源码中的usage的支持不怎么样。因此到这里线索断了。

再辟新径

没办法,只能一个个看api包下的其他混淆类。这里因为是源码是用class文件编译出来的,所以高级搜索没法用。
幸运的是,我们在e类中找到了请求的api。因为在抓包的时候,我们发现客户端会对/api/client/rsa进行获取rsa公钥。我们在e类中发现了这个地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public BSGameSdkAuth b(final Context var1) throws BSGameSdkExceptionCode, HttpException, IOException {
c var2 = new c<BSGameSdkAuth>() {
public BSGameSdkAuth b(String var1x) throws BSGameSdkExceptionCode, HttpException, IOException {
Builder var2 = Uri.parse(var1x).buildUpon();
var2.path("/api/client/rsa");
Map var3 = e.c(var1, 1);
this.a(var3);
e.this.a(var1, var3, var1x);
e.c(var3);
BSGameSdkAuth var4 = new BSGameSdkAuth();
Uri var5 = var2.build();
HttpPost var6 = HttpDNSConfig.queryCachePost(var5.toString(), var3);
var6.addHeader("User-Agent", "Mozilla/5.0 BSGameSDK");
String var7 = HttpManager.executeForString(var1, var6);
var4.parseRSAResponse(var7);
return var4;
}
};
return (BSGameSdkAuth)var2.a(0, a.s(), "rsa", (String)null);
}

主要还是看中间的几行

1
2
3
4
Map var3 = e.c(var1, 1);
this.a(var3);
e.this.a(var1, var3, var1x);
e.c(var3);

先看e.c方法。cr^b进入方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
private static Map<String, String> c(Context var0, int var1) {
a();
HashMap var2 = new HashMap();
var2.put("game_id", com.bsgamesdk.android.model.c.a);
var2.put("merchant_id", com.bsgamesdk.android.model.c.f);
var2.put("version", "1");
var2.put("timestamp", "" + com.bsgamesdk.android.api.b.b());
var2.put("client_timestamp", "" + System.currentTimeMillis());
var2.put("server_id", com.bsgamesdk.android.model.c.g);
var2.put("sdk_ver", com.bsgamesdk.android.model.c.l);
var2.put("sdk_type", com.bsgamesdk.android.model.c.s);
var2.put("c", a.c());
var2.put("isRoot", com.bsgamesdk.android.model.c.q);
var2.put("udid", com.bsgamesdk.android.model.c.p);
var2.put("support_abis", com.bsgamesdk.android.model.c.r);
var2.put("mac", TextUtils.isEmpty(ab.d()) ? "" : ab.d());
var2.put("imei", TextUtils.isEmpty(ab.b(var0)) ? "" : ab.b(var0));
var2.put("android_id", TextUtils.isEmpty(l.b(var0)) ? "" : l.b(var0));
boolean var3 = com.bsgamesdk.android.b.b.checkIsLogined(var0);
boolean var4 = com.bsgamesdk.android.b.b.checkIsTouristLogined(var0);
String var5 = "";
String var6 = "";
if (var3) {
UserParcelable var7 = (new m(var0)).c();
var5 = var7.uid_long + "";
var6 = var7.access_token + "";
} else if (var4) {
TouristUserParceable var10 = (new com.bsgamesdk.android.model.k(var0)).c();
var5 = var10.uid_long + "";
var6 = var10.access_token + "";
}

var2.put("uid", var5);
var2.put("access_key", var6);
var2.put("app_id", com.bsgamesdk.android.model.c.a);
var2.put("sdk_log_type", "1");
var2.put("ver", com.bsgamesdk.android.model.c.i);
var2.put("version_code", com.bsgamesdk.android.model.c.j);
var2.put("channel_id", com.bsgamesdk.android.model.c.c);
h.a(var2);
var2.put("platform_type", "3");
var2.put("model", h.c(var0));
var2.put("brand", h.d(var0));
var2.put("net", h.a(var0));
var2.put("operators", h.b(var0));
var2.put("pf_ver", h.e(var0));
var2.put("dp", com.bsgamesdk.android.model.c.t);

try {
String var11 = TextUtils.isEmpty(o.a(var0).c) ? "" : o.a(var0).c;
String var8 = TextUtils.isEmpty(o.a(var0).d) ? "" : o.a(var0).d;
var2.put("old_buvid", var11);
var2.put("cur_buvid", var8);
} catch (Throwable var9) {
var2.put("old_buvid", "");
var2.put("cur_buvid", "");
LogUtils.printThrowableStackTrace(var9);
}

if (TextUtils.isEmpty(b)) {
b = com.bsgamesdk.android.utils.a.a(var0);
}

var2.put("apk_sign", b);
var2.put("oaid", TextUtils.isEmpty(com.bsgamesdk.android.model.c.z) ? "" : com.bsgamesdk.android.model.c.z);
var2.put("fingerprint", TextUtils.isEmpty(com.bsgamesdk.android.model.c.A) ? "" : com.bsgamesdk.android.model.c.A);
return var2;
}

这个方法中,参数var1是个遗留参数,并没有使用。应该只是为了重载方法。这个方法的作用就是添加各种参数,这些参数在抓包的时候param里面基本上都有。除了个别参数。和sign无关,往回看。接下来是两个this.a方法,也都是添加参数。
接下来是另一个c方法。e.c(var3);

1
2
3
4
private static void c(Map<String, String> var0) {
String var1 = d(var0);
var0.put("sign", var1);
}

很好,这可能是我们需要的,继续跟进d方法。

1
2
3
4
5
6
private static String d(Map<String, String> var0) {
String var1 = a(var0);
String var2 = "";
var2 = p.a(var1, com.bsgamesdk.android.model.c.b);
return var2;
}

先看第一行String var1 = a(var0);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static String a(Map<String, String> var0) {
ArrayList var1 = new ArrayList(var0.keySet());
Collections.sort(var1);
String var2 = "";

for(int var3 = 0; var3 < var1.size(); ++var3) {
String var4 = (String)var1.get(var3);
if (var4 != null && !var4.equalsIgnoreCase("item_name") && !var4.equalsIgnoreCase("item_desc")) {
String var5 = (String)var0.get(var4);
var2 = var2 + var5;
}
}

return var2;
}

逻辑很简单,对map的参数key进行排序,然后把value拼接到一起。
接着看var2 = p.a(var1, com.bsgamesdk.android.model.c.b);

1
2
3
4
5
6
7
8
9
10
11
12
public static final String a(String var0, String var1) {
try {
String var2 = var0 + var1;
MessageDigest var3 = MessageDigest.getInstance("MD5");
var3.update(var2.getBytes(Charset.defaultCharset()));
byte[] var4 = var3.digest();
return a(var4);
} catch (NoSuchAlgorithmException var5) {
LogUtils.printExceptionStackTrace(var5);
return "";
}
}

其实就是把两个参数拼接起来然后进行MD5摘要。这就是我们的sign。
总结:sign就是所有参数排序后保留value然后拼接com.bsgamesdk.android.model.c.b的值进行hash。
那新的问题来了,这个model.c.b的值又是什么呢?

接近答案

跟进model.c类,b是一个静态String,而b的赋予在c类的a方法中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void a(Context var0, String var1, String var2, String var3, String var4, String var5, String var6, boolean var7, String var8) {
f = var1;
a = var2;
g = var3;
d = var7;
i = f(var0);
j = g(var0);
k = var5;
l = var6;
m = var8;
b = var4;
c = d(var0);
e = com.bsgamesdk.android.utils.o.a(var0).a();
//省略部分代码
}

之后就没有了,既然反向不行,那就从正向开始。
我们找到demo的MainActivity,跟进官方的文档,必须在oncreate中调用BSGameSdk.initialize方法完成sdk的初始化。我们跟进initialize方法。

1
2
3
4
5
6
7
8
9
public static BSGameSdk initialize(boolean var0, Activity var1, String var2, String var3, String var4, String var5, InitCallbackListener var6, ExitCallbackListener var7) {
if (f == null) {
com.bsgamesdk.android.utils.f.a(var1.getApplicationContext());
f = new BSGameSdk(var0, var1, var2, var3, var4, var5, var6, var7);
}

return f;
}

跟进BSGameSdk

1
2
3
4
5
6
7
@SuppressLint({"NewApi"})
private BSGameSdk(boolean var1, Activity var2, String var3, String var4, String var5, String var6, final InitCallbackListener var7, ExitCallbackListener var8) {
//省略部分代码
com.bsgamesdk.android.model.c.a(var2, var3, var4, var5, var6, var9.getSDK_NAME(), var9.getSDK_Version(), var9.isBiliSDK(), var9.getVersion());
//省略部分代码

}

我们找到了刚才model.c的中a方法的调用方。
a方法中我们需要的值是参数中的var4也就是a方法中的第5个参数。对应到BSGameSdk构造方法就是var6,而var6在这则是第6个参数,再往上走是initialize方法,第6个参数是var5,也是第6个参数。
最后是MainActivity调用方,第6个参数对应的是app_key,至此破案。

总结

我们从源码中得到了sign的计算方式,是一堆参数排序后取value,然后拼接app_key后进行md5摘要。刚开始的HttpQueryMap可能是针对get方法在其他地方使用到,毕竟进行了url转义,当然也可能是一个遗留类,我们不得而知。
这次源码分析还是比较顺利,主要还是源码没有强加密和混淆,使得分析更加容易些。

原文作者:Zer0e

原文链接:https://re0.top/2021/04/05/bsgamesdk-re/

发表日期:April 5th 2021, 2:10:00 pm

更新日期:April 5th 2021, 4:40:54 pm

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 准备工作
    2. 2.2. 分析过程
    3. 2.3. 初露曙光
    4. 2.4. 再辟新径
    5. 2.5. 接近答案
  3. 3. 总结