[JS逆向] 喜马拉雅登录案例
博客配套代码发布于github:喜马拉雅登录 (欢迎顺手Star一下⭐)
相关知识点:webpack 补环境
相关爬虫专栏:JS逆向爬虫实战 爬虫知识点合集 爬虫实战案例 逆向知识点合集
此案例目标为逆向成功对应的参数,并成功返回对应请求。
一、爬取工作准备
进入网址:喜马拉雅登录,再点击右上角那个小电脑,进入密码登录界面。
手机号输入123456789,密码输入123456,点开F12,再点击登录,就可以开始我们的表演了。
一看就知道这个就是我们要的数据,再看载荷:
能看出我们大致要破解的四个参数:account,nonce,password,signature.
老规矩,直接复制这个数据包到curl to python,并将其复制到py运行查看结果(步骤在以往JS逆向案例有,不懂的直接看往期操作即可)
可以看到运行结果应是这个。如果并非这个结果说明参数传递错误。
二、逆向入口分析
直接encrypt搜,搜到七个,分析发现只有下面四个长得像,依次打上断点。
只剩这俩地方断的住。
试着从第二个开始找,我们直接选择最下层的这个匿名,进去查看下:
巧了不是,account,password,nonce都看到了,
这儿还有这么完美的四大参数,不是这我吃。现在再挨个分析即可。
account:
account是n,n是→这里getEncryptPwd点进去的确可以进入到下层栈然后接着找,再补环境,但未免落了下成。往上再捋捋看看a的位置在哪。一直翻翻翻,终于看到:
看到a=r(19),webpack与加载器,看到这就很开心了,后面可以直接一套公式流webpack过掉,轻轻又松松啊。
password:
password是i,i是→跟上面的account近乎一个样,也是webpack,处理近乎一致。
nonce:
nonce是t,t是→
呃,是传入参数。而且这个t还是个一直变动的值。找不到上下层堆栈。
别急:当你看到检测参数较多而且某个值似乎无法通过溯源找到适,可以看看其他的数据包:
果然,是从另一个数据包获得的nonce,那我们只需要通过单独向这个数据包发请求获得nonce即可。
signature:
siganture是e,e是→可以将其简化,即:
var e = a.getSignature({
account: n,
password: i,
nonce: t
})
乍一看,也是用了a加载器,直接像上面的账密一样...不行!
这里有个坑,而且非常隐蔽,笔者在这里就捣鼓了好久才知晓原因。
如果你用的是加载器的方式得到,它最后作为参数是错误的。原因极大概率是因为其获取方式与常规加载器的逻辑不同。这里说一下此处的分析思路应如何。
正常来讲是会去想到直接去用加载器,但用前一定先在控制台打印观察下。此处可以看到打印出来格式为哈希算法,则:尽可能别用加载器,加密算法的逻辑就单独用算法方式解决,两者获取可能会冲突。
点进a.getSiganture看对应层逻辑:
这里选中的是e.toUpperCase()),能看到它是一个很长的由上方拼接获取的字符
同时我们再把这一串打印进控制台:
发现没有:在同一时间线(同个ajax请求)下,获得的两个算法最终答案相近,但后者(加密算法)才对。
同时这里能通过分析哈希算法长度,即40位,基本确定其为sha1算法。
但还是有点不放心。我们这里是想直接用sha1的加密逻辑得到,但不确定它有没有加盐(魔改)算法,到SHA1加密网络转换器里,将e.toUpeerCase()的东西输入进去看下(记得去掉引号)
与上述答案一致。那么就可以放心直接用算法了,至此这个逻辑基本完成。
综上,所有参数分析完毕后可以开始我们的正式操作了。
三、破解逆向
1.补webpack
这段就老生常谈的公式了,尽量简化描述:
(如需更详细的步骤请参考我往期写的webpack处理公式)
选中加载器,刷新页面并进去里面,全选复制粘贴过来并简单处理掉里面的html元素,并在顶部写个window=global。再把刷新进去位置的函数(function i(r)),设置个console.log('r:::',r)与window.loader=i
最后再将传入代码写作:
一般如果webpack里是r('GSGX')这种还能依靠直接搜'GSGX'复制粘贴过来,但如果是像上图这种r(19)的webpack,就得通过源函数来进入了:
选中r(19):进入里面这个栈并全局复制到mod01即可(对应的是数组,不用分析半天究竟是谁)
缺这玩意儿就全局搜并全部复制(注意这个搜索的是var xx,切记把这个var去掉不然没法上升为全局变量),再给个mod02补上,并require过来(注意require的顺序):
看到这里就能确定webpack环节结束,接下来到补环境阶段。
2.补环境
却啥补啥,补到此时:
有点怪,好像不太好补。那就把我们的监控器递出来吧↓
(监控器具体用法在文章 [逆向知识] 补环境 -- 让本地逆向如鱼得水 处有讲,建议在这里看一看)
function setProxy(proxyObjArr) {for (let i = 0; i < proxyObjArr.length; i++) {const handler = `{get: function(target, property, receiver) {console.log("方法:", "get ", "对象:", "${proxyObjArr[i]}", " 属性:", property, " 属性类型:", typeof property, ", 属性值:", target[property], ", 属性值类型:", typeof target[property]);return target[property];},set: function(target, property, value, receiver) {console.log("方法:", "set ", "对象:", "${proxyObjArr[i]}", " 属性:", property, " 属性类型:", typeof property, ", 属性值:", value, ", 属性值类型:", typeof target[property]);return Reflect.set(...arguments);}}`;eval(`try {${proxyObjArr[i]};${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});} catch (e) {${proxyObjArr[i]} = {};${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});}`);}
}setProxy(['window', 'document','canvas'])
document补全,看到:
很明显知道canvas少了个toDataURL,加进去即可。
同样,其他补环境逻辑也大致相同,陆续为setProxy加东西即可,只有一个点要注意下:
大大的userAgent,这玩意儿就不要再传空值或空func,直接控制台打印个丢过来不好吗
这个点也是不太好处理,但只要全局搜就可以了,搜到发现隶属于document对象,仿写过来就行。同理,后面好几个环境也可以靠这样补。
最终成功补完没再报错,将setProxy的代理与之前console.log的r都注释掉,
这个是window对象,也补上环境就消失了,
最终输出:账户与密码的逻辑搞定。
3.py与js代码互传
首先我们要确定py与js互传的方式。分为execjs与subprocess。正好这里都会用到。execjs用于构建环境较简单,没有很多浏览器原生的bom/dom,api或更复杂的模拟环境这些;subprocess则用于如之前代码中复杂的环境模拟,最终输出。
(1) execjs
它的使用方式相较非常简单,直接传递即可。这里我们以siganture的获得举例
cryptoJs = require("crypto-js")function y(t) {var e = "", r = Object.keys(t).sort((function (t, e) {return (t = t.charCodeAt(0)) - (e = e.charCodeAt(0))})), n = r.length;return r.forEach((function (r, o) {var i = t[r];e += "".concat(r, "=").concat(i),o < n - 1 && (e += "&")})),e
}function getSignature() {var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, e = y(t) + "&" + "WEB-V1-PRODUCT-E7768904917C4154A925FBE1A3848BC3E84E2C7770744E56AFBC9600C267891F";return e.toUpperCase(),cryptoJs.SHA1(e.toUpperCase()).toString()
}
这个signature在js中的构建逻辑,而在py中,我们只需要通过如下的代码即可获取对应js生成的代码。
js_compile = execjs.compile(open('sign.js',encoding='utf8').read())
data = {'account': enc_account,'password': enc_password,'nonce': nonce_val}
signature = js_compile.call('getSignature',data)
其中data是传递进去的参数,'getSignature'则是用于选择这个文件的这个函数。
(2) subprocess
其使用相较就复杂很多,这里也是分别放js与py文件对应代码:
function get_all(account, password) {function get_account() {return (0, a.getEncryptPwd)(account)}function get_pwd() {return (0, a.getEncryptPwd)(password)}return {account:get_account(),pwd:get_pwd(),}
}act = process.argv[2]
pwd = process.argv[3]
const ret = get_all(act,pwd)
res = JSON.stringify(ret)
console.log(res)process.exit()
在js文件中,通过process.argv获取传递过来的原始账密并传递给get_all,再生成后打印到控制台上。py文件再通过控制台的消息对应接收过来。本质上可以理解为双方是靠终端来接收各自的数据。
all_ = subprocess.check_output(f'node get_sign.js "{account}" "{password}"') # 分别获得传递
all_ = all_.decode('utf8').strip()
# 通过json传递取出对应值
js_data = json.loads(all_)
account = js_data['account']
pwd = js_data['pwd']
注意:这里我用的是json数据来互传。因为js传递过来的数据是个长字符串,用json格式才方便将其转化为字典格式以提取。
四、完整逻辑构建
如上,我们基本已完成了相关具体逻辑与所有代码的构建。
最终我们将其模块化打包输出:
if __name__ == '__main__':account = '123456789'password = '123456'cur_nonce = get_nonce()enc_account,enc_pwd = get_encrypt(account, password)signature = get_signature(enc_account,enc_pwd,cur_nonce)ret = login(enc_account,enc_pwd,cur_nonce,signature)print(ret)
答案获取成功,逆向完成。
由于代码量过大,本文并未将所有代码都放上来。但可以看我的github开源代码,来进一步全面分析。
📌 项目代码 + 后续案例合集 全部发布在 GitHub 仓库中,持续更新中,欢迎收藏!