转载

PicaComic接口分析手记

好久没有更新技术类文章了,不过其实我也有慢慢在写几篇文章,然而它们依旧躺在草稿箱。刚好群里讨论写个Pica客户端,于是我就来分析下Pica的接口吧。

拆开来一看,竟然没有混淆……build.gradle改改也没多大成本吧,虽然给我省事就是了。

抓包得知,Pica的接口使用signature头以校验。所以首先要找出signature的计算方法。由于,Pica采用了okhttp3,所以signature的计算逻辑八成会写在interceptor里。搜索addInterceptor,果然只有一处,出现在com.picacomic.fregata.networks.RestClient的构造器里。

// 局部变量名称与部分逻辑有所调整
Request request = chain.request();
String nonce = UUID.randomUUID().toString().replace("-", "");
String path = request.url().toString().replace("https://picaapi.picacomic.com/", "");
String timeStamp = System.currentTimeMillis() / 1000L + PreferenceHelper.getTimeDifference(var1) + "";
String signature = MyApplication.getInstance().getStringCon(new String[]{"https://picaapi.picacomic.com/", path, timeStamp, nonce, request.method(), "C69BAF41DA5ABD1FFEDC6D2FEA56B", RestClient.version, RestClient.buildVersion});

于是顺藤摸瓜,看com.picacomic.fregata.MyApplication的getStringCon方法。

public String getStringCon(String[] strs) {
    if (this.generateSignature == null) {
        this.generateSignature = new GenerateSignature();
    }
 
    String rawParams = "";
    for (String str : strs) {
        rawParams += str + ", ";
    }
 
    PrintLog.PrintErrorLog(TAG, "RAW parameters = " + rawParams);
    String concatParam = this.getStringConFromNative(strs);
    PrintLog.PrintErrorLog(TAG, "CONCAT parameters = " + concatParam);
    String concatKey = this.getStringSigFromNative();
    PrintLog.PrintErrorLog(TAG, "CONCAT KEY = " + concatKey);
    return this.generateSignature.getSignature(concatParam, concatKey);
}

getStringConFromNative和getStringSigFromNative两个方法都是native的,那暂且放一遍,先把Java层的getSignature看完。不过逻辑其实也不难猜到,就是转小写后HmacSHA256。所以重点还应该是在native层。不过有点挺有趣的,getStringSigFromNative很好理解,但getStringConFromNative怎么看都像是拼接字符串。在这上面做文章还是没怎么见过,有意思。

lib名是libJniTest,很优秀。丢进IDA,很顺利。找到函数,很顺利,毕竟静态注册。F5,依旧很顺利。嘛,看到lib名也应该能想到的。首先分析Java_com_picacomic_fregata_MyApplication_getStringConFromNative,依旧是当初分析B站App的套路,改类型、重命名,然后分析逻辑。其实也什么好分析,首先从数组中取出对应下标的字符串然后GetStringUTFChars,然后就是主要的拼接逻辑。

PicaComic接口分析手记

拼接很直接,就是这个用于判断的repack_chk_and_genKey10(原名genKey10)需要分析一哈。通过分析,发现这个函数是用来校验apk签名的。

PicaComic接口分析手记

然而校验失败返回false后,getStringConFromNative依旧可以拼接,而且会变更拼接的方式。至于为什么这么做,笔者暂且被蒙在吉他里。

接下来分析Java_com_picacomic_fregata_MyApplication_getStringSigFromNative。

PicaComic接口分析手记

……

PicaComic接口分析手记

心凉半截。看看汇编。

PicaComic接口分析手记

没办法,按照esp的偏移慢慢算咯。当然傻傻的一个个字符改也确实没效率,所以简单处理一下数据,写个py jio本。

char_dic = {0x9: 110, 0x2D: 107, 0x37: 97, 0x3B: 107, 0x40: 114, 0x46: 114, 0xC: 83, 0x14: 85, 0x19: 76, 0x1B: 82, 0x27: 67,
            0x28: 69, 0x2C: 75, 0x33: 90, 0x45: 67, 0xD: 57, 0x16: 56, 0x1F: 57, 0x21: 52, 0x23: 51, 0x2F: 55, 0x38: 53, 0x42: 49}
int_dic = {0x17: "zf", 0x29: "sl", 0x39: "zk", 0x1D: "PM", 0x31: "BY",
           0x35: "BA", 0x0F: "lG", 0x11: "ts", 0x3C: "RB", 0x3E: "L7"}
 
if __name__ == "__main__":
    offset = 0x8
    org_str = "~*}$#,$-/").=$)/",,#/-.'%(;$[,|@/&(#/"~%*!-?*/"-:*!!*,$/"%.&'*|%/*,*"
    ls = list(org_str)
    for pos, ch in char_dic.items():
        ls[pos - offset] = chr(ch)
    for pos, ch in int_dic.items():
        ls[pos - offset] = ch[0]
        ls[pos - offset + 1] = ch[1]
    print("".join(ls))

这放飞自我的编码风格……嘛,总之算是跑出了结果。另外,校验失败同样会返回另一个key。

简单汇总一下signature的计算方法:

  1. 拼接请求路径(不包含/)+当前时间戳+nonce+请求方法+”C69BAF41DA5ABD1FFEDC6D2FEA56B”
  2. 转小写
  3. 计算其HmacSHA256,密钥为”~n}$S9$lGts=U)8zfL/R.PM9;4[3|@/CEsl~Kk!7?BYZ:BAa5zkkRBL7r|1/*Cr”

其中nonce生成的java实现为UUID.randomUUID().toString().replace(“-“, “”);,常见的随机串生成方式。实现时生成任意32长随机字符串即可。

总体感觉就是有安全意识但是做的很不够。不过讲字符串拼接的逻辑放在native层很有趣,而且native层的处理较B站早期的实现也更完善。改天测试下另一种signature有啥不同。

原文  https://blog.kaaass.net/archives/1074
正文到此结束
Loading...