前言
最近看到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转义,当然也可能是一个遗留类,我们不得而知。
这次源码分析还是比较顺利,主要还是源码没有强加密和混淆,使得分析更加容易些。