美团滑块-[behavior] 加密分析
文章仅为学习交流,如有侵权请联系删除

定位加密
- 直接搜索关键字即可
- 源JS也是做了一层OB混淆,建议先进行AST解混淆会对会面的分析有很大帮助
- 没用很难的检测点但很多,补接混淆的话很费心力
- 怎么解自行研究一下、我对这方面研究也不深
- 解后的样子
- 触发看一下参数
- 三个参数
- data就是滑动生成的轨迹信息
- jc是request_code
- this.config.isDegrade是返回的
- 可以在前面的包返回中找到
- 整个JS扣下来补环境即可
JS分析
-
下面都以解OB混淆后的JS演示
-
上面定位到了生成位置、分析一下生成逻辑
-
处于自执行底部,赋值
window.Yoda.slider = HR;
-
HR 实际调用到是jg, 在jg处打个断点看看参数
-
这里的参数还是前面page_data返回的data
-
这里可以大概知道调用方式为:
window.Yoda.slider(page_data);
-
继续看jg这个对象
-
大概可以猜测处这里是滑块相关的配置和调用函数
-
滑倒末尾调用
stopDrag
-
然后调用
dealMove
->onStop
,就到了前面定位的加密位置 -
可以看到data的初始化的对象
-
轨迹部分最简单的办法,拿伪代码让AI大模型写个伪造轨迹结果的代码就搞定了
-
这里把jg的伪代码贴出来,方便测试
function jg(jf) { var jI = jW.call(this, jf) || this; jI.doms = {}; jI.count = 0; jI.globalTimer = 0; jI.timeoutCount = 0; jI.firstTimeStamp = 0; jI.moveingBarX = 0; jI.moveingBarY = 0; jI.maxLeft = 0; jI._x = 0; jI._y = 0; jI.actualMove = 0; jI.initTimeStamp = Date.now(); jI.isDrag = false; jI.data = { trajectory: [], env: { Type: 0, Return: 0, zone: [], client: [], Timestamp: [], count: 0, timeout: 0 } }; jI.customStyle = {}; jI.ids = { help: "yodaHelp", boxWrapper: "yodaBoxWrapper", box: "yodaBox", moveingbar: "yodaMoveingBar", tip: "yodaSliderTip" }; jI.whiteDuration = 0; jI.sliderMaxLenth = 100; jI.sliderType = 0; jI.sliderReturn = 0; jI.oceanPoint = []; jI.init = function () { var jM = jI.config; var jK = jM.action; var jF = jM.type; var jP = jM.style; var jN = jM.requestCode; var jJ = jM.root; var jO = jM.t; var jo = jM.display; jI.customStyle = jP || {}; if (aV(jI.customStyle.slider) === "object") { jI.customStyle = jI.customStyle.slider; } try { var jQ = new aY(jI.config); var jS = jQ.template(jI.ids, jI.customStyle || {}, vX(jI.config)); vx(jJ, jS, jo); } catch (jp) { vE(jp, jK, jF); return; } jI.whiteDuration = Date.now() - jI.config.yodaInitTime; jI.doms = vC(jI.ids); if (typeof jO === "number") { jI.sliderType = jO; } var jA = function jU(jD, jX) { return Math.round(Math.random() * (jX - jD)) + jD; }; if (jO === 2) { var jk = jI.doms.boxWrapper; var jY = jk.getBoundingClientRect().width; var js = Math.ceil(jY / 100 * 70); var jZ = Math.floor(jY / 100) * 100; var jB = jA(js, jZ); jk.style.width = jB + "px"; } if (jO === 3) { var je = jA(50, 70); jI.sliderMaxLenth = je; } jI.initSlider(jI.doms.box, jI.doms.boxWrapper); var jn = { duration: jI.whiteDuration, method: bT.SLIDER, mtaction: "loading", action: jK, requestCode: jN }; vK(jn); if (jI.config.mounted) { vz(jI.config.mounted); } // change // jI.createbgImage("https://s3plus.meituan.net/v1/mss_f231eb419c414559a1837748d11d4312/yoda-resources/slider/m_loading.png", jK).then(function () {}); // TOLOOK setTimeout(function () { try { RQ("slider", jN); } catch (jX) { var jD = jX; window.Yoda.CAT.sendLog(location.href, "jsError", jD.message, jD.stack || "", "info"); } }, 0); }; jI.initSlider = function (jM, jK) { jI.drag = jM; jI.moveingBar = jI.doms.moveingbar; jI.maxContainer = jK; H8(jI.doms.box, "mousedown", jI.startDrag); H8(jI.doms.box, "touchstart", jI.startDrag); H8(jI.doms.box, "touchstart", function () { window.Yoda.CAT.sendLog(location.href, "jsError", "PC上显示了i版的滑动", "使用了touchstart事件触发了滑块", "info"); }); var jF = { action: jI.config.action, type: jI.config.type, yodaInitTime: jI.config.yodaInitTime, whiteDuration: jI.whiteDuration }; Hy(jF); if (typeof jI.config.mounted === "function") { jI.config.mounted(); } }; jI.help = function () { var jM = "https://verify.inf.test.meituan.com/feedback/manmachine/#/?requestCode=" + jI.config.requestCode; window.open(jM); }; jI.startDrag = function (jM) { jI.count++; clearTimeout(jI.globalTimer); jI.timeoutListen(); if (!jI.firstTimeStamp) { jI.firstTimeStamp = Date.now(); } jI.moveingBarX = jI.moveingBar.clientWidth; jI.maxLeft = jI.maxContainer.clientWidth - jI.drag.offsetWidth; if (jM.clientX) { jI._x = jM.clientX; jI._y = jM.clientY; } else { jI._x = jM.targetTouches[0].clientX; jI._y = jM.targetTouches[0].clientY; } H8(document, "mousemove", jI.moveDrag); H8(document, "mouseup", jI.stopDrag); H8(document, "touchmove", jI.moveDrag); H8(document, "touchend", jI.stopDrag); H8(document, "mousemove", jI.sliderMoveDrag); H8(document, "mouseup", jI.sliderStopDrag); H8(document, "touchmove", jI.sliderMoveDrag); H8(document, "touchend", jI.sliderStopDrag); H9(jI.doms.box, "mousedown", jI.startDrag); H9(jI.doms.box, "touchstart", jI.startDrag); var jK = jI.maxContainer; var jF = { startX: vM(jI._x), startY: vM(jI._y), w: vM(jK.clientWidth), h: vM(jK.clientHeight), clientX: vM(jK.getBoundingClientRect().left), clientY: vM(jK.getBoundingClientRect().top) }; jI.onStart(jF); Hm(jM); }; jI.timeoutListen = function () { jI.globalTimer = window.setTimeout(function () { clearTimeout(jI.globalTimer); if (!jI.isDrag) { jI.stopDrag(); if (jI.data) { jI.delLastItem(jI.data.trajectory); } jI.timeoutCount++; } }, 3000); }; jI.move = function (jM) { var jK = 0; var jF = 0; if (jM.clientX) { jK = jM.clientX; jF = jM.clientY; } else if (jM.targetTouches) { jK = jM.targetTouches[0].clientX; jF = jM.targetTouches[0].clientY; } var jP = { clientX: jK, clientY: jF }; return jP; }; jI.sliderMoveDrag = function (jM) { var jK = jI.move(jM); var jF = jK.clientX; var jP = jK.clientY; jI.oceanPoint.push([0, jF, jP, Date.now() - jI.initTimeStamp]); }; jI.sliderStopDrag = function (jM) { var jK = jI.move(jM); var jF = jK.clientX; var jP = jK.clientY; jI.oceanPoint.push([0, jF, jP, Date.now() - jI.initTimeStamp]); H9(document, "touchmove", jI.sliderMoveDrag); H9(document, "touchend", jI.sliderStopDrag); H9(document, "mousemove", jI.sliderMoveDrag); H9(document, "mouseup", jI.sliderStopDrag); jI.sliderPCPoint(JSON.stringify(jI.oceanPoint)); jI.oceanPoint.splice(0); }; jI.moveDrag = function (jM) { var jK = jI.move(jM); var jF = jK.clientX; var jP = jK.clientY; var jN = jF - jI._x; var jJ = jP - jI._y; if (Math.abs(jN) < 6 && Math.abs(jJ) < 6) { return false; } if (jN < 0) { jN = 0; } if (jN > jI.maxLeft) { jN = jI.maxLeft; } if (jI.sliderMaxLenth !== 100 && jN / jI.maxContainer.clientWidth * 100 > jI.sliderMaxLenth) { jI.sliderReturn = jN; jI.sliderMaxLenth = 100; jN = 0; Hm(jM); jI.stopDrag(); } jI.setBoxPosition(jN); jI.onMove(vM(jF), vM(jP)); if (jN === jI.maxLeft) { jI.stopDrag(); } Hm(jM); }; jI.stopDrag = function () { H9(document, "mousemove", jI.moveDrag); H9(document, "mouseup", jI.stopDrag); H9(document, "touchmove", jI.moveDrag); H9(document, "touchend", jI.stopDrag); jI.dealMove(); }; jI.setBoxPosition = function (jM) { jI.drag.style.left = jM + "px"; jI.moveingBar.style.width = jI.moveingBarX + jM + "px"; jI.actualMove = jM; }; jI.dealMove = function () { if (jI.actualMove === jI.maxLeft) { jI.isDrag = true; H9(jI.drag, "mousedown", jI.startDrag); H9(jI.drag, "touchstart", jI.startDrag); jI.actualMove = 0; jI.drag.className = "boxLoading " + (jI.customStyle.boxLoading || ""); jI.onStop(); return false; } jI.backToStart(); }; jI.boxOk = function () { jI.drag.className = "boxOk " + (jI.customStyle.boxOk || ""); }; jI.boxStatic = function () { jI.drag.innerHTML = ""; jI.drag.className = "boxStatic " + (jI.customStyle.boxStatic || ""); jI.moveingBar.className = "moveingBar " + (jI.customStyle.moveingBar || ""); }; jI.boxError = function () { jI.drag.className = "boxError " + (jI.customStyle.boxError || ""); jI.moveingBar.className = "moveingBarError " + (jI.customStyle.moveingBarError || ""); }; jI.backToStart = function () { var jM = 0; var jK = // TOLOOK setInterval(function () { var jF = vw.easeOutCubic(jM * 17, 0, jI.actualMove, 500); var jP = jI.actualMove - jF; jI.drag.style.left = jP + "px"; jI.drag.style.left = jP + "px"; jI.moveingBar.style.width = jI.moveingBarX + jP + "px"; if (jP <= 0) { jI.drag.style.left = "0px"; jI.drag.style.left = "0px"; jI.moveingBar.style.width = jI.moveingBarX + "px"; jI.actualMove = 0; clearInterval(jK); H8(jI.drag, "mousedown", jI.startDrag); H8(jI.drag, "touchstart", jI.startDrag); } jM++; jI.boxStatic(); }, 17); }; jI.onStart = function (jM) { var jK = jM.startX; var jF = jM.startY; var jP = jM.w; var jN = jM.h; var jJ = jM.clientX; var jO = jM.clientY; jI.data.env.zone = [jP, jN]; jI.data.env.client = [jJ, jO]; jI.data.trajectory.push({ point: [[0, jK, jF, Date.now() - jI.initTimeStamp]], vector: { orientation: "h" } }); jI.oceanPoint.push([0, jK, jF, Date.now() - jI.initTimeStamp]); var jQ = { action: jI.config.action, method: "71", requestCode: jI.config.requestCode }; vN(jQ); }; jI.onMove = function (jM, jK) { var jF = jI.data.trajectory; if (Array.isArray(jF) && jF.length) { jF[jF.length - 1].point.push([0, jM, jK, Date.now() - jI.initTimeStamp]); } }; jI.showMessage = function (jM) { jI.doms.tip.textContent = jM; Ha(jI.doms.tip); var jK = window.setTimeout(function () { clearTimeout(jK); Hh(jI.doms.tip); }, 3000); }; jI.config = jf; var jd = jf.theme || "meituan"; if (typeof window.yodaTheme === "function") { window.yodaTheme(jd); } jI.init(); return jI; } jg.prototype.onStop = function () { var jf = this; var jI = this.data.trajectory.length - 3; this.data.trajectory = this.data.trajectory.slice(jI > 0 ? jI : 0); this.data.env.Timestamp = [this.initTimeStamp, this.firstTimeStamp]; this.data.env.count = this.count; this.data.env.timeout = this.timeoutCount; this.data.env.Type = this.sliderType; this.data.env.Return = Number(this.sliderReturn.toFixed(0)); var jc = this.config.requestCode; var jz = { id: bT.SLIDER, request_code: jc, behavior: Rx(this.data, jc, this.config.isDegrade), fingerprint: "", action: this.config.action }; this.verify(jc, jz).then(function (jE) { if (jE) { var jd = jE.message; var jM = jE.code; var jK = vn.isNeedSwap(jM); jf.boxError(); if (!jK) { jf.showMessage(jd); } if (jM !== "jump" && jM !== 121056 && !jK) { // TOLOOK setTimeout(function () { jf.backToStart(); }, 1000); } } else { jf.boxOk(); } }); }; jg.prototype.delLastItem = function (jf) { if (Array.isArray(jf) && jf.length) { jf.length = jf.length - 1; } };
-
好在轨迹路径不多、伪代码也还算清晰、构造伪造函数应该难度不大
-
可以提交几段生成的data数据辅助生成
-
贴个例子
-
这里应该不算做难点
-
参数部分都搞明白了,接着看加密部分
-
behavior: Rx(this.data, jc, this.config.isDegrade),
-
原代码是滑块滑到末尾后调用加密、发送请求,没有返回
-
可以定义个公共变量赋值加密函数、方便在外部调用
-
后面就常规补环境就行了
-
代理监控函数不会写可以去前面的文章找找,这个没啥特别的、用着方便就行。
-
后面其实没啥特别难的环境、但是用了很多很多环境。。。
-
不再一步步跟着补,还是贴一些关键点
-
插件取name值
-
canvas
画图部分就取了这么多属性。。。 -
并且是验证了两次、也就取了两次toDataURL
-
因为两次的结果是不一样的,我的处理方案是定义个全局公共变量做判断
-
其实这里我认为不重要,正常todataIRL是不会验证结果的、只会验证有或没有、区别就在生成的加密,应该是不会影响使用的(只是想法未测试,为了对比结果我是返回的正确dataURL)
-
然后还有个window.f
-
一个自执行函数,直接扣下来放在顶部就行了
-
其它照常补就行了、缺啥补啥、日子打清晰就不会漏
-
对了、如果补着没日志输出了,就要处理一下
console
, 被覆盖修改了、不能正常打印 -
处理方法也很简单
-
Object.freeze(console);
-
对象冻结
-
环境部分就这些。
-
补充一点参数的处理
-
config就是page_data返回的,替换掉options中requestCode就行了
-
然后调用就是
result = window.Rx(data,window.seed.config.request_code, window.seed.config.isDegrade);
-
公共函数 Yoda
总结-验证
- 补环境不是难度活而是体力活!!!
- 心细+心累=成功
- 最后来个验证
- 请求滑块,得到page_data
- 替换到补的环境中
window.seed.config
- 滑动滑块触发加密
- 拷贝data值做结果对比测试
- 对比结果
- 因为结果是固定的、所以可以对比
- 结果一致、可用性就不用考虑了吧
- 请求滑块,得到page_data
- 再测试自生成轨迹、打断点替换生成值
- 还是先拿到page_data
- 生成加密值
- 然后替换掉behavior
- 注意看替换的值和页面生成的值是不一样的
- 提交看结果
- 正常返回request_code
- 结尾说下_token参数,也是轨迹加密、之所以这么长是因为轨迹数据比较大,没啥特别的,另外这个参数没用到、不用管。