本帖最后由 slslsl 于 2024-6-13 19:02 编辑
前言
由于昨天发的 某壁纸小程序sign逆向分析 文章中,有很多朋友回复问到库迪的sign值分析,没有具体研究思路
于是乎晚上睡前抓包看了下,应该和之前一样算是同样的类型吧,故记录下分析思路以及具体流程供大家学习参考
事先分析
第一步也是同样,有了目标小程序之后,我们先打开抓包工具,这里我用的依然是proxypin,可以直接支持对pc端小程序进行抓包,点左上角开启抓包之后,打开目标小程序
可以看到已经有疑似的流量进来了,重点关注这里的域名,一会儿会用到
那么抓到请求之后,展开域名分组,随便找一个请求,看到发出去的具体headers里面存在我们所需要分析的sign值
这里有点经验的同学应该都不难看出,这个sign值可能是一个32位大写的经过MD5计算之后的值(当然看不出来也没关系,就先大胆猜想一波这个值肯定是经过了某种加密或者编码方法之后得到的吧)
那么为了验证这个请求是否每次都需要携带正确的sign值才能成功(当然有很小的概率后端接口没有做校验,就任凭你随意传一个sign上去都能成功返回数据),我们这里重放一下这个请求看看结果如何
ok,重放请求之后,返回了错误提示信息,那么可以确定的是,接口肯定是做了请求时间的校验,当请求携带的时间戳参数值与当前真实时间偏差太大的话,就会得到错误响应
那么我别出心裁地将header里面的timestamp参数给手动改成最新的,再重放一次请求,试试能不能行
当然不出意外,我们将时间戳改为最新的时间之后,总不可能再提示我们“时间不正确”了吧,于是乎这里得到了一个新的提示,“MD5 signature cannot be verified”
MD5签名无法被验证
这里可以得到两个有效信息:
①sign值确实是使用的md5算法进行签名得到,验证了我们之前的猜想
②这里的sign值应该是与时间戳有关,或者说,计算sign值的时候有时间戳的参与
事前的分析就到这里。
目标
那么这里我们的目标就已经很明确了,就是要分析上面请求当中的sign值具体是怎么来的
因为每一次的请求都必须要携带上正确的sign值以及最新的时间戳,才能通过验证,所以我们需要找到,到底有那些东西经过了md5加密之后得到最终正确的sign值
只有当我们能够成功计算出正确的sign值,请求才可能会正常返回数据
思路
这里我们已经完成了小程序请求抓包,并且涉及到针对sign加密算法的分析,所以需要去进行解包尝试拿到可读代码。
wx小程序的解包套路目前几乎都是比较固定的:
①获取到小程序包***.wxapkg文件
②对文件进行解密 (pc端获取的需要进行这一步,手机端的好像是不用解密直接可以进行解包)
③对解密出来的文件进行解包
④打开微信开发者工具导入解包后的项目进行分析
准备工具
抓包:proxypin
获取小程序包***.wxapkg文件,这里我用的是pc微信,比较省事儿
解包:unveilr
分析:微信开发者工具
抓包
这一步因为在上面“事先分析”的部分已经做过了,这里就直接跳过进入下一阶段,开始解包
解包
在pc微信上将目标小程序打开之后,尽量把所有能点的页面都先点一遍,防止部分小程序存在分包的情况导致未能完全加载
然后点击pc微信左下角设置--->文件管理,找到对应路径目录下的Applet
进入后可以看到很多小程序的appid文件夹,按日期排序后的第一个应该就是,直接进入
经过我们乱点一通之后,已经把所有分包文件都加载出来了,现在看到的应该是这样
这里就得到了需要解包的小程序***.wxapkg文件以及所在路径,未分包的一般只有__APP__.wxapkg这个主包,其余的便是分包
这里我使用unveilr工具直接在命令行中指定小程序路径,进行解包,用法如下
[Bash shell] 纯文本查看 复制代码 .\unveilr.exe "D:\WeChat Files\Applet\wxe766d738ad655e8c\98"
经过一段时间处理之后,成功得到了解包之后的小程序项目工程__APP__,进入查看大致结构,确定没问题,还是熟悉的味道
接下来直接打开微信开发者工具,导入当前项目尝试运行起来
这里特别注意一下,正常情况下最好先将本地设置里面的这个选项去掉,避免项目编译出现一堆报错无从下手
项目运行起来之后,不出意外都是会有报错的,看到下面满满飘红的错误信息,可能觉得无从下手,但是不必惊慌
因为我们只需要分析其中的代码逻辑,不需要真正将项目给运行起来(当然碰到一部分项目必须触发某个按钮或者访问某个页面的时候,就得想办法让项目跑起来再去分析了)
接下来就是我们的重头戏,很多同学到这一步往往就不知道该怎么办了,这里给大家提供几条可行的入手点:
入手点1:从“sign”关键词入手,直接找到对应的声明定义或者赋值的部分
入手点2:从请求路径或者参数入手,找到具体调用发起请求的位置
入手点3:从app.js入手,查看里面的初始化方法以及具体逻辑
入手点4:从页面路径或者某个按钮组件入手,定位到其页面结构的位置,然后再顺藤摸瓜找到对应触发的方法函数
按我个人习惯,直接从入手点1开始,直接全局搜索下sign看看究竟都在哪里出现过
(这里有一个技巧,因为我们已经知道sign是变量的情况下,其赋值肯定是以 "sign:" 或者 "sign =" 的形式出现的,而不是直接搜索 "sign",这样能够避开很多干扰信息,例如assign、signIn、signType等等,这些肯定不是我们所需要的)
对比看下:
可以很明显看到,搜索sign: 关键词直接帮我们排除了很多无效信息,并且这里有必要的情况下甚至还可以打开后面的大小写区分搜索,结果更精确,不过这里的结果已经足够少了,就直接开始定位分析了
点开最前面几条,可以看到这里均是对开发、测试、生产等不同环境下的配置定义,其中包括了ak、sk、version三个参数,以及具体的请求域名baseUrl
那么这里似乎可以发现,我们之前抓包的请求域名“https://gateway.cotticoffee.com”似乎就在其中
这里继续往下,跳过第五条paySign:的结果,也是属于干扰结果,直接定位到第六条结果的位置
这里已经很明显了,我们所需要的sign值计算正是在这个关键位置,能够找到这里就已经成功了一大半了。
接下来对这里的代码简单分析下:
sign: o.default.hex_md5_32Upper(s.join(""))
参与计算的参数是s变量,这里用到了join(""),那么自然能想到s变量应该是一个数组的形式
关于前面的o.default.hex_md5_32Upper() 方法,其实就已经可以不去深究了,因为整体就是一个md5加密计算的方法,得到计算结果,那不如就把它直接看作md5(s.join("")),更便于理解
现在继续往上,可以直接看到s变量的定义以及赋值的过程
这里对c变量的成员进行了一个遍历,将其中的每个成员键名与键值进行了拼接,然后再最后push了一个r变量进去
那这里的c变量和r变量是什么呢,继续往上找
看到这里其实就已经一目了然了,ak?sk?version?不正是最前面几个搜索结果所定义的不同环境下的配置参数吗
e变量通过getConfig()拿到配置信息中的sign定义具体参数,然后分别将其中的几个值赋给了临时变量
实际经过还原之后大致示意如下:
[JavaScript] 纯文本查看 复制代码 c = {
timestamp: (new Date).getTime(),
path: t,
version: e.version
}
这里实际上就是将c变量中的所有值进行按键名从小到大排序之后,然后把键名与键值一一拼接得到一个新的数组
根据之前抓包得到的请求域名,我们将其对应的变量ak与sk参数代入,path不清楚是啥,姑且先用"123"来代替吧,于是乎可以得到如下的结果
可以看到c变量的三个成员键名与键值牢牢地贴在了一起
但是别忘了,s计算完之后,还push了一个r变量到尾部,也就是将sk参数push进去了
最后调用.join("")方法,把当前的s变量所有值拼接成字符串,并且对其进行md5计算32位大写签名结果
得到这样一串字符串,以及md5后的结果8A1DF57488445416B7453E57AE2EF766
这里的结果当然是不对的,因为我们的c变量path参数只是临时用"123"去替代的
这里我们先将之前抓包的请求对比看一下
这里我们假设path值就是上面的请求地址,并且将timestamp的值也代入刚才的计算方法中,看看能不能成功计算得到正确的sign值:1B55C19B8AC79987C8A26D4C52DB1968
也就是将这段字符串进行md5计算
pathhttps://gateway.cotticoffee.com/cotti-capi/shop/homePageGetShopDetailtimestamp1718261976864version1.0.0k9k16jSJaXj46QLElEVT8uRme2uHrtee
得到的结果是并不是我们所期望的那样
现在所有参与md5计算的结果当中,除了path的值以外,其余的值都是确定应该没问题的,那我们尝试改变一下path?或许不用传域名呢?毕竟之前的变量里面还单独定义了一个baseUrl
尝试去掉域名之后的字符串进行md5计算
path/cotti-capi/shop/homePageGetShopDetailtimestamp1718261976864version1.0.0k9k16jSJaXj46QLElEVT8uRme2uHrtee
那么逆向过程中最激动人心的时刻,往往就是在这一刹那,它出现了!
1B55C19B8AC79987C8A26D4C52DB1968!没错,和请求抓包的结果一样!这也就表明了我们的尝试是正确的
最后让我们来构造计算一个新的sign值(注意这里的a变量已经换成了最新的时间戳)
经过加密之后得到md5的值
最后在重放请求header中填入对应的sign值和timestamp,发送,成功得到了正确的返回数据
大功告成!
总结
今天这个小程序的sign逆向分析过程呢,其实存在一些巧合,也就是有一定的运气成分,特别是对于path路径传入的具体值猜测,往往在其他分析过程中,可能会碰到多一个/或者少一个什么符号,导致始终无法得到正确的签名结果,这是很常见的。
感兴趣的同学呢,也可以继续在代码中去找这个path具体是怎么来的,为什么恰好就是去掉了域名之后的请求路径
当然往往在分析得没有任何头绪的时候,不妨大胆一点,去猜测,去大胆假设,小心论证,幸运可能不经意间,就会降临到你的头上
同样今天这个小程序的sign值分析,也很值得刚入门的同学们去练手,梳理清楚整个分析的过程,一步一步沉下心慢慢来,相信你们也能成功体会到成功的愉悦 |