32

PicaComic接口分析手记

 5 years ago
source link: https://blog.kaaass.net/archives/1074?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

好久没有更新技术类文章了,不过其实我也有慢慢在写几篇文章,然而它们依旧躺在草稿箱。刚好群里讨论写个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,然后就是主要的拼接逻辑。

z63QVzn.png!web

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

eEZbIrm.png!web

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

接下来分析Java_com_picacomic_fregata_MyApplication_getStringSigFromNative。

mArMFrF.png!web

……

QZfIVfz.png!web

心凉半截。看看汇编。

qAvUnuf.png!web

没办法,按照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有啥不同。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK