前期准备
首先,需要一个干净的浏览器环境,最好使用firefox隐私模式,而不是chrome,firefox的防跟踪模式比chrome更加隐蔽,不过对于b站来说,指纹收集并没有那么变态,我们可以直接使用chrome系浏览器打开,这里我使用的是edge。
打开b站登陆页面后,提前f12,勾选禁用缓存\保留日志\禁用断点,并准备一个api调试工具,利用reqable或apifox之类的调试软件可以轻松对参数进行验证。
请求分析
输入用户名和密码,然后触发登陆请求,对内容进行分析,这里可以看到请求内容还是较多的,我们点击搜索按钮,对登陆的账号进行搜索,这里可以快速定位到请求
将payload复制下来,可以看到如下参数:
source: main-fe-header
username: 1234567890
password: Q3tKLCiEJpkKMPmwGkbW9uCYQGLoVSKHU7xOQl/TZRVnSI5bcyhzJ5RQY3zHE9Xnwc+KSCm1TAiRYRnlbz5auTn9Gy+g5fu7q1Q0jtQSnFJkLLa6oU5JwKW0iMcFPXL65X1DGHgzVZtu5cNfCdw59g8mFNyb0YYoN0XoKa3MjGg=
go_url: https://www.bilibili.com/
validate: b0e487504e38b7410b70c9aff2703e8c
token: 27ec028a018949c8a415dc24ef16be3f
seccode: b0e487504e38b7410b70c9aff2703e8c|jordan
challenge: d821f3a9927cfefe30e556824d7a3c9c
将请求参数进一步分割,对好理解的几项进行标记,此处可以明显看出下列四项参数的意义
source: 请求来源
username: 账号
password: 密码
go_url: 跳转链接
其中passowrd为base64编码,在我们将其进行解码后发现转换的字符串是乱码
C{J,(&
0Fֶb蕢SNB_ӥgH[s(s'Pc|ǓէOH)La対Z9/廻T4ԒRd,NI%Dž=r彃x3Un僟 ܹ&ܛц(7E詭̌h
这种情况下,经常做逆向的同学可能就发觉了,这个应当是通过rsa或aes等方式加密后的数据,由于加密后的密文为二进制,所以需要通过base64编码后序列化传输,我们如果需要登陆,就需要寻找一下密码加密的来源
而剩余的参数,很明显是验证码了,这里这四个是极验的参数
validate: b0e487504e38b7410b70c9aff2703e8c
token: 27ec028a018949c8a415dc24ef16be3f
seccode: b0e487504e38b7410b70c9aff2703e8c|jordan
challenge: d821f3a9927cfefe30e556824d7a3c9c
插桩点分析
切换到源代码面板,并在工具栏添加全局搜索标签,全区搜索password,可以看到在四个文件中搜到了关键字,共计有4各文件50多个结果,搜索结果不算多,但是人为进行分析的话,也是非常麻烦的一件事,这时我们就要利用一些技巧来进行取舍了。例如,下方的搜索结果中log-reporter.js,一般是用于前端插桩分析的,我们可以直接跳过;pc.js为js模块,我们暂不清楚内容,但文件内只有一个关键词搜索结果,分析起来较为简单;剩下的两个结果较多的,熟悉vue开发的同学,能够看出来这两个是vue构建的前端文件,和加密逻辑相关的内容很有可能在这里面
在搜索密码时,password一般作为变量或对象成员、json键出现,因此我们也可以改进我们的搜索手段,例如通过搜索password =
、 password :
这种方式来进一步缩小范围。可以看到,这里一下减少了大量的内容,并且依据我们的经验,可以直接排除掉日志监控模块log-reporter.js,那么这样下来就只有3处需要分析了
这里有两处可以直接排除
最后剩下的内容就比较可疑了,这里构造的o对象,成员包含我们在上文分析的source、go_url等字段,那么基本可以从这里开始分析了
代码逻辑逆向
在代码前后打上断点,此时果然出现了我们需要的内容,不过奇怪的是,我们之前相同的密码登陆,参数值为Q3tKLCiEJpkKMPmwGkbW9uCYQGLoVSKHU7xOQl/TZRVnSI5bcyhzJ5RQY3zHE9Xnwc+KSCm1TAiRYRnlbz5auTn9Gy+g5fu7q1Q0jtQSnFJkLLa6oU5JwKW0iMcFPXL65X1DGHgzVZtu5cNfCdw59g8mFNyb0YYoN0XoKa3MjGg=
,而这次登陆却不一样,说明在密码加密过程中,可能加入了时间戳等内容作为盐值,或是rsa加密
跟栈发现,r为方法内定义的值,而非外部传入
逐级向上跟踪调用栈,这里有个小技巧,可以在debugger的监视面板中追寻base64消失的位置,这里一般就是参数开始生成的地方。这里找到了base64消失的位置,对Si断点,并对Si内部的s进行插桩日志输出,可以看到Si在被调用到第二次时出现了我们需要的内容,切换到Si进行分析,这里有个麻烦的点是Si内部生成了一个Promise和Generater,之间相互跳转,需要有点跟栈经验才能够不被绕进去
找到Eo,发现Eo传入了我们的原始密码,并且里面有一个非常可疑的JSEncrypt和setPublicKey,并且整段代码逻辑的控制流被破坏掉了,用一个while-switch流来控制,这大概就是我们需要的内容了,这里的e.prev = e.next为混淆语句,直接插桩输出e.next的值即可
还原一下控制流,去除无用分支,死代码,并把标识符回填。需要注意一下传入的e是会变化的,代码逻辑大概为
s = new JSEncrypt()
s.setPublicKey(o.data.key),
r(s.encrypt(o.data.hash + t)))
查看执行结果,s.encrypt(o.data.hash + t)) 为密文,向上寻找,查看o的值,o明显为rsa加密密钥。
因此o.data.hash为rsa明文的盐值,t为我们的密码明文。再次查看执行结果,并跟断点到我们一开始找到密码的地方,发现果然一致
盐值寻找
再运行几次,发现有个问题出现了,即密码明文加的盐值不一样,每次运行时都会改变
继续跟栈,找到密钥传入的栈底为a,传入的t为密钥及盐值
我们再跟一跟,查看他周围的函数。到这里实际上就有点绕了,t.apply(e,n)实际上的意思是t(n),但将t的上下文替换为了this的上下文,n是父函数的参数,也就是co返回的那个匿名函数的参数
而co返回的函数,实际上被传入了一个promise,所以匿名函数的参数应该是resolve和reject,因此传入t的参数也是resolve和reject,最后一套下来,实际上就是执行了一个promise
明明是生成盐值,为什么要返回一个Promise呢?这时候有聪明的同学想起之前的内容就知道是怎么回事了,哎,你说巧不巧,o这个对象里面,z有一个code属性,这是不是很像http接口返回的数据呢?
搜索一下,果然发现了端倪,实际上这个哈希值和密钥,都是通过key接口拿取的
使用reqable将请求拦截,替换为我们的参数验证一下,果然可以请求到数据
密码的加密这样就完成啦!