本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,联系作者立即删除!
最近巴黎奥运会,很多平台都搞起了免费饮品免单的活动,当然雪王也不例外,小程序与 App 同为 webview 类型的程序,起初该平台只有一个 sign 加密,最近压力上来了,更新了一个新参数 type_1286 ,不少粉丝也被这个参数难住了,本文仅对雪王的这个新参数进行逆向分析,仅供学习交流**。**
某雪冰城小程序、某雪冰城 App
I+Wwj+eoi+W6jzovL+icnOmbquWGsOWfji9hb1RwNU1zT0tJMHRHWWM=
APP 端的 webview 调试主要借助 XP 模块,或者 LSP。然后导入 webview 模块即可调试, 以 LSP 为例,模拟器装好面具以及 LSP 后,将 webview 模块导入并且选择指定 APP:
然后重启模拟器,在浏览器输入chrome://inspect/#devices
,若无设备加载出来,则在 cmd 控制台多输入几次 adb devices
直到设备加载出来:
然后点击 inspect 即可进入,出现正常的 F12 界面证明调试成功:
首先 USB 数据线连接手机进入调试模式;
首先微信访问 http://debugxweb.qq.com/?inspector=true
确定是否可以用(能打开就能用);
微信上打开你需要调试的页面;
谷歌浏览器地址栏输入 chrome://inspect/#devices
等待一会儿 (浏览器需要具备翻强功能);
点击对应网页或者小程序 inspect 即可出现调试栏,然后像正常调试页面即可chrome://inspect/#devices
。
最后效果如 App 端开启调试的结果相同。
进入免单界面,点击领取,在开发者工具中即可查看到该接口,如下:
其主要是加密参数为 url 接口中的 type_1286,以及提交内容中的 sign,本文将重点讲 type_1286 参数的生成。
该参数为领取免单券接口的重要参数,我们在 App 或者小程序端输入口令点击确认,观察堆栈,从第一个堆栈进入:
然后我们在进入的地方下一个断点,然后继续点击确认,发现在此处断了下来,发现是一个大 OB 混淆,与普通的OB 还不太一样,经过分析可知 UL 参数即为我们需要逆向的参数:
`var UL = UE['Fu'](this[oA(P7.a)][-0x190d + -0x20e9 + 0x39f7]) , UL = F0[oA(P7.A)](UH, UL, UV);`
在俩个 UL 参数中下断点,再次刷新点击确认,成功在 UL 处断了下来,经过分析可知,第一个 UL 为一个 object 对象,然后将 UL,UV 参数传入UH中完成加密,生成最终的 url 如下:
我们进入 UH 函数进行分析,发现它是一个 && 用法,最终通过 M['F6'](L, N)
生成加密参数 type_128,也就是调用 F6 函数生成最终的加密参数:
我们进入 F6 函数,观察它的生成逻辑:
发现它也是被混淆的不成样子了,最后加密参数由以下代码块生成:
`(g += N), (N = F[UJ(mS.F)](F[UJ(mS.Y)](F[UJ(mS.U)](F[UJ(mS.a)](F[UJ(mS.A)](M[UJ(mS.D)](g), '|'), (-0xfbf + 0x9 * -0x189 + 0x16 * 0x158, m['n'])()), '|'), new Date()[UJ(mS.o)]()), '|1'), g = E['FU']['ua'](N, !(0xbb4 + 0x1a49 * -0x1 + 0xe95)), N = {}), (N[M['F7'](L[UJ(mS.i)])] = g, L[UJ(mS.y)] = (-0x2ff + 0xbe5 + -0x8e6, H['Fa'])(L[UJ(mS.y)], N), (0x3 * 0xe5 + -0x614 + 0x1 * 0x365, H['FY'])(L))`
所以我们想要拿下这个参数就要将这个 F6 函数拿下,这里我们讲 2 种方法补环境和算法还原,如果细分第二种办法又可以分为两种。
关于补环境的话,我们直接将代码全部拿下,放到浏览器里跑一下试试:
没错,浏览器卡死了,甚至电脑的风扇都开始转个不停:
返回网页 js,我们发现在 F3 函数中存在格式化检测:
我们将代码压缩,放到 node 环境中执行一次,看看能不能正常报错:
发现我们的代码已经可以正常跑起来了,接下来就到缺啥补啥的环境了,还是老样子,将代理挂上:
``memory = { 'Proxy': true, 'random': 0.5, } // Math.random = function(){return memory['random']}; memory.proxy = (function() { memory.Object_sx = ['Date']; memory.Function_sx = []//['Array', 'Object', 'Function', 'String', 'Number', 'RegExp', 'Symbol', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'Uint8Array']; memory.setFun = []; memory.getObjFun = []; memory.color = { 'set': [3, 101, 100], 'get': [255, 140, 0], 'has': [220, 87, 18], 'apply': [107, 194, 53], 'ownKeys': [147, 224, 255], 'deleteProperty': [199, 21, 133], 'defineProperty': [179, 214, 110], 'construct': [200, 8, 82], 'getPrototypeOf': [255, 255, 255], 'object': [147, 224, 255], 'function': [147, 224, 255], 'number': [255, 224, 0], 'array': [147, 224, 0], 'string': [255, 224, 255], 'undefined': [255, 52, 4], 'boolean': [76, 180, 231], }; memory.log = console.log; memory.log_order = 0; memory.proxy_Lock = 0; // 文本样式 function styledText(text, styles) { let styledText = text; // RGB颜色 if (styles.color) { styledText = `\x1b[38;2;${styles.color[0]};${styles.color[1]};${styles.color[2]}m${styledText}\x1b[0m`; } // 背景颜色 if (styles.bgColor) { styledText = `\x1b[48;2;${styles.bgColor[0]};${styles.bgColor[1]};${styles.bgColor[2]}m${styledText}\x1b[0m`; } // 粗体 if (styles.bold) { styledText = `\x1b[1m${styledText}\x1b[0m`; } // 斜体 if (styles.italic) { styledText = `\x1b[3m${styledText}\x1b[0m`; } // 下划线 if (styles.underline) { styledText = `\x1b[4m${styledText}\x1b[0m`; } // 返回带样式的文本 return styledText } // 文本填充 function limitStringTo(str, num) { str = str.toString() if (str.length >= num) { return str + ' ' } else { const spacesToAdd = num - str.length; const padding = ' '.repeat(spacesToAdd); // 创建填充空格的字符串 return str + padding; } } // 进行代理 function new_obj_handel(target, target_name) { if(memory.Proxy == false){return target}; let name = target_name.indexOf('.') != -1 ? target_name.split('.').slice(-1)[0]: target_name; if (target['isProxy'] || memory.Object_sx.includes(name)) { return target; }else{ return new Proxy(target,my_obj_handler(target_name)) } } function new_fun_handel(target, target_name) { if(memory.Proxy == false){return target} let name = target_name.indexOf('.') != -1 ? target_name.split('.').slice(-1)[0]: target_name; if (memory.Function_sx.includes(name)) { return target; }else{ return new Proxy(target,my_fun_handler(target_name)) } } // 获取数据类型 function get_value_type(value) { if (Array.isArray(value)) { return 'array' } if (value == undefined) { return 'undefined' } return typeof value; } // 函数与对象的代理属性 function my_obj_handler(target_name) { return { set: function (obj, prop, value) { if(memory['proxy_Lock']){ return Reflect.set(obj, prop, value); }; const value_type = get_value_type(value); const tg_name = `${target_name}.${prop.toString()}`; const text = limitStringTo(++memory['log_order'],5) + limitStringTo('setter',20) + limitStringTo(`hook->${tg_name};`,50) // 如果设置到的属性是对象 --> 输出值对象 // 如果设置到的属性是方法 --> 输出值function // 其他的就全部输出值 if (value && value_type === "object") { memory.log(styledText(text, { color: memory.color['set'], }), styledText('value->',{ color: memory.color['set'], }),value) } else if (value_type === "function") { memory.setFun.push(tg_name) memory.log(styledText(text , { color: memory.color['set'], }),styledText(`value->`, { color: memory.color['set'], }),styledText(`function`, { color: memory.color[value_type], })) } else { memory.log(styledText(text, { color: memory.color['set'], }),styledText(`value->`, { color: memory.color['set'], }),styledText(`${value}`, { color: memory.color[value_type], })) } return Reflect.set(obj, prop, value); }, get: function (obj, prop) { if(memory['proxy_Lock']){ return Reflect.get(obj, prop) }; if (prop === "isProxy") { return true; } const value = Reflect.get(obj, prop); const tg_name = `${target_name}.${prop.toString()}`; const value_type = get_value_type(value); const text = limitStringTo(++memory['log_order'],5) + limitStringTo('getter',20) + limitStringTo(`hook->${tg_name};`,50) // 如果获取到的属性是对象 --> 对其getter和setter进行代理 // 如果获取到的属性是方法 --> 对其caller进行代理 // 其他的就全部输出值 if (value_type === 'object') { if (memory.getObjFun.indexOf(tg_name) == -1){ memory.log(styledText(text, { color: memory.color['get'], }), styledText('value->',{ color: memory.color['get'], }),value) memory.getObjFun.push(tg_name) } return new_obj_handel(value,tg_name) } else if(value_type === "function"){ if (memory.getObjFun.indexOf(tg_name) == -1){ memory.log(styledText(text , { color: memory.color['get'], }),styledText(`value->`, { color: memory.color['get'], }),styledText(`function`, { color: memory.color[value_type], })) memory.getObjFun.push(tg_name) } return new_fun_handel(value,tg_name); } else{ memory.log(styledText(text , { color: memory.color['get'], }),styledText( `value->` , { color: memory.color['get'], }),styledText( `${value}` , { color: memory.color[value_type], })) return value } }, has: function(obj, prop) { if(memory['proxy_Lock']){ return Reflect.has(obj, prop) } const value = Reflect.has(obj, prop); const value_type = get_value_type(value); const text = limitStringTo(++memory['log_order'],5) + limitStringTo('in',20) + limitStringTo(`hook->"${prop.toString()}" in ${target_name};`,50) memory.log(styledText(text, { color: memory.color['has'], }), styledText(`value->`, { color: memory.color['has'], }), styledText(`${value}`, { color: memory.color[value_type], })) return value; }, ownKeys:function(obj){ if(memory['proxy_Lock']){ return Reflect.ownKeys(obj); } const value = Reflect.ownKeys(obj); const value_type = get_value_type(value); const text = limitStringTo(++memory['log_order'],5) + limitStringTo('ownKeys',20) + limitStringTo(`hook->${target_name};`,50) memory.log(styledText(text, { color: memory.color['ownKeys'], }), styledText(`value->`, { color: memory.color['ownKeys'], }), styledText(`${value}`, { color: memory.color[value_type], })); return value }, deleteProperty:function(obj, prop) { if(memory['proxy_Lock']){ return Reflect.deleteProperty(obj, prop); } const value = Reflect.deleteProperty(obj, prop); const tg_name = `${target_name}.${prop.toString()}`; const value_type = get_value_type(value); const text = limitStringTo(++memory['log_order'],5) + limitStringTo('delete',20) + limitStringTo(`hook->${tg_name};`,50) memory.log(styledText(text, { color: memory.color['deleteProperty'], }), styledText(`value->`, { color: memory.color['deleteProperty'], }), styledText(`${value}`, { color: memory.color[value_type], })); return value; }, defineProperty: function (target, property, descriptor) { if(memory['proxy_Lock']){ return Reflect.defineProperty(target, property, descriptor); }; const value = Reflect.defineProperty(target, property, descriptor); const tg_name = `${target_name}.${property.toString()}`; const text = limitStringTo(++memory['log_order'],5) + limitStringTo('defineProperty',20) + limitStringTo(`hook->${tg_name};`,50) memory.log(styledText(text, { color: memory.color['defineProperty'], }), styledText('value->',{ color: memory.color['defineProperty'], }),descriptor) return value; }, getPrototypeOf(target) { if(memory['proxy_Lock']){ return Reflect.getPrototypeOf(target); } var value = Reflect.getPrototypeOf(target); const text = limitStringTo(++memory['log_order'],5) + limitStringTo('getPrototypeOf',20) + limitStringTo(`hook->${target_name};`,50) memory.log(styledText(text, { color: memory.color['getPrototypeOf'], }), styledText('value->',{ color: memory.color['getPrototypeOf'], }),value) return value; } }; } function my_fun_handler(target_name) { return { apply:function(target, thisArg, argumentsList){ if(memory['proxy_Lock']){ return Reflect.apply(target, thisArg, argumentsList); }; if(memory.setFun.indexOf(target_name) != -1 || memory.setFun.includes(target_name.split('.')[0])){ // 扣的代码触发 var value = Reflect.apply(target, thisArg, argumentsList); memory.setFun.push(`log_${memory['log_order'] + 1}`) } else{ // 补的环境触发的分支 memory['proxy_Lock'] = 1 var value = Reflect.apply(target, thisArg, argumentsList); memory['proxy_Lock'] = 0 } const value_type = get_value_type(value); const text = limitStringTo(++memory['log_order'],5) + limitStringTo('caller',20) + limitStringTo(`hook->log_${memory['log_order']} = ${target_name}();`,50); memory.log(styledText(text, { color: memory.color['apply'], }),styledText('arguments->',{ color: memory.color['apply'], }),argumentsList, styledText('returnValue->',{ color: memory.color[value_type], }),value) if(value_type == 'object'){ return new_obj_handel(value,`log_${memory['log_order']}`); } else if(value_type == 'function'){ return new_fun_handel(value,`log_${memory['log_order']}`); } return value; }, construct: function (target, args, newTarget) { if(memory['proxy_Lock']){ return Reflect.construct(target, args, newTarget) } if(memory.setFun.indexOf(target_name) != -1 || memory.setFun.includes(target_name.split('.')[0])){ var value = Reflect.construct(target, args, newTarget); memory.setFun.push(`log_${memory['log_order'] + 1}`) } else{ memory['proxy_Lock'] = 1 var value = Reflect.construct(target, args, newTarget); memory['proxy_Lock'] = 0 } const text = limitStringTo(++memory['log_order'],5) + limitStringTo('new',20) + limitStringTo(`hook->log_${memory['log_order']} = new ${target_name}();`,50) memory.log(styledText(text, { color: memory.color['construct'], }), styledText('arguments->',{ color: memory.color['construct'], }),args, styledText('returnValue->',{ color: memory.color['construct'], }),value); return new_obj_handel(value, `log_${memory['log_order']}`); }, } } // 返回进行对象代理 return new_obj_handel }()); ``
其中,检测最多的就是 createElement
校验了对标签的创建,以及标签下面的属于,检测较深但不严:
全部补完大概在 400 行代码左右,最后运行代码没有报错,那么我们的环境就已经补好了,如下:
那么我们应该如何调用加密函数呢?还记得刚刚的 F6 函数吗?我们将代码全部放到 Notepad 中,将代码进行改写,将 F6 函数导出:
然后将代码全部复制,放到在线 js 代码压缩网站中进行压缩,将压缩后的 js 代码放到我们刚刚补的环境下面,打印 console.log(window.kk)
看看我们的函数有没有导出:
最后将参数我们传入到加密函数中,不出所谓,打印出了正确的结果:
补环境的话需要考虑的地方很多,对某些节点检测甚至有 3-4 层的深度,全部拿下的话代码行数在 8000 行左右,下面我们用算法还原的方式将整个算法进行剖析。
我们还是回到加密函数 F6 中,看看它具体是通过哪些步骤进行加密的:
首先将解密函数赋值给了 UJ,然后将 L 传入 H['FY'] 中进行取值,跟进 H['FY'] 看看它做了什么:
最终 var g = L["FW"] + L["hash"];
然后 g += N
,接着:
`N = F[UJ(mS.F)](F[UJ(mS.Y)](F[UJ(mS.U)](F[UJ(mS.a)](F[UJ(mS.A)](M[UJ(mS.D)](g), '|'), (-0xfbf + 0x9 * -0x189 + 0x16 * 0x158, m['n'])()), '|'), new Date()[UJ(mS.o)]()), '|1') `
前面仍然是混淆的 + 函数,最后分析可得:
`N = (((((sig(g) + '|') + 0) + '|') + new Date()["getTime"]()) + '|1') `
然后调用 E['FU']['ua'](N, !(0xbb4 + 0x1a49 * -0x1 + 0xe95))
完成最后的加密:
所以分析可知,我们需要先拿下 sig 函数,进到 sig 中,发现其结构如下:
`'sig': function(L) { var Ub = Uc; for (var N = 0xa52 + 0x1499 + 0x1 * -0x1eeb, g = F[Ub(mn.F)](encodeURIComponent, L), B = 0x129f + 0x7 * 0x3d + -0x144a; F[Ub(mn.Y)](B, g[Ub(mn.U)]); B++) N = F[Ub(mn.a)](F[Ub(mn.A)](F[Ub(mn.D)](F[Ub(mn.o)](N, 0xb7a * -0x2 + 0x25c7 * 0x1 + -0xecc), N), -0x1bf6 + -0xb06 * -0x1 + 0x127e), g[Ub(mn.i)](B)), N |= 0x6b * 0x35 + 0x1349 * 0x2 + -0x3cb9 * 0x1; return N; }`
还原以后为:
`function sig(L) { for (var N = 0, g = encodeURIComponent(L), B = 0; B < g["length"]; B++) N = ((((N << 7) - N) + 398) + g["charCodeAt"](B)), N |= 0; return N; } `
接着,我们再进入主加密函数 ua 中,结构如下,依旧是吃相极其难看的代码:
在代码中,我们看到了调用了解密函数,以及 uu 等函数调用,ua 分析后可得:
`function ua(E, H) { var W = ["3", "4", "2", "1", "0"] , P = 0; while (!![]) { switch (W[P++]) { case '0': switch (M["length"] % 4) { default: case 0: return M; case 1: return (M + "==="); case 2: return (M + '=='); case 3: return (M + '='); } case '1': if (H) return M; continue; case '2': var M = uu(E, 6, function(L) { return V["uGGDj"].charAt(L); }); continue; case '3': var K = {}; K["uGGDj"] = "DGi0YA7BemWnQjCl4+bR3f8SKIF9tUz/xhr2oEOgPpac=61ZqwTudLkM5vHyNXsVJ"; var V = K; continue; case '4': if (null === E) return ''; continue; } break; } } `
接着进入 uu 函数中,一如既往的吃相更难看的代码:
这就完了?不,还有:
前面几个函数我们都是手动替换的解密后的字符串,所以解密函数就没有扣,那看看这个函数,你再手动替换试试看,手估计要费掉,所以必须将解密函数 F3 拿下,然后它就可以调用 az 自主完成字符串的解密。当然解密函数就是本文难点之一。
前面我们说的算法分析可以粗略的分为俩种,其实就是解密函数的扣法可以分为俩种,我们进入解密函数 F3 中,观察其结构如下:
它接收两个参数 a 和 A,通过对 a 进行复杂的算术操作来计算一个新的索引值 n:
然后从数组 F 中取出该索引处的元素,并返回这个元素。数组 U 也在计算过程中被用来获取中间值。特定值的函数。
将 F3 分析后复现如下:
`F3 = function(a, A) { a = a - (-0x256c + -0x23 * -0x67 + -0x17f3 * -0x1); var r = U[a]; var o = U[0x1b * 0xd3 + -0x2 * -0x1189 + 0xb77 * -0x5] , n = a + o , i = F[n]; r = i; return r; } `
所以我们可以将 U 和 F 拿下,U 数组很好拿下,因为他就是偏移量之后的数据,但是 F 就不一样了,如果你在很早之前就将 F 拿下,那么可能会造成 F 缺失的问题,会导致解密函数某些值解密不成功,因为F是一直在增加的
所以我们如果想拿下完整的 F,就要在加密完成之前或者接近加密结束的时候进入 F3 中,将 F 全部控制台 copy 下来,这样整个解密函数将被彻底拿下:
在 uu 函数中同样存在 lw 和 F 。同时 F 函数中需要用到 UZ 函数,我们只需定义一个空的 UZ 函数即可:
`function UZ(a) { } `
最后整个加密流程整合,即可出现正确的结果:
出现 OB 大数组所占的行数,整个算法也就几百行左右,比起之前的代码可谓是相当整洁。
刚刚我们提到在扣解密函数的时候,可能会由于时机不正确,导致复制下来的 U 有所缺失,那么就会造成解密失败,如下:
所以我们在网页中找到该位置对应的 js 代码,将解密函数失败的 az(lw.V)
这种值全部找出来,然后我们放到自己定义的一个大对象 KKK 中,如下:
`KKK = { 1719:"qMjri", 1093:"BEYhV", 267:"bdCZp", 1463:"HXHPX", 1016:"hzLDX", 1364:"aBzMZ", 507:"YFbJS", 1118:"pow", 1010:"Jwfww", 1483:"wZcNG", 1024:"UYeav", 927:"tLwiW", 230:"tQdqh", 1880:"dOdjo", 1614:"IlkJY", 410:"PvxjR", 1867:"tYdmC", 1670:"llrMA", 1577:"wEEdD", 1593:"uhTrx", 1860:"cbQeM", 1734:"rfswT", 594:"NUkoa", 877:"charAt", } `
然后我们用代理器给我们的解密函数挂上代理, 通过代理(Proxy)对象为 F3
对象添加一个自定义的函数调用处理器。它利用 KKK
这个映射对象,在某些条件下替代函数的返回值:
`// 定义处理函数 kk_handler const kk_handler = { apply: function(target, thisArg, argumentsList) { // 原始函数调用 let result = target(...argumentsList); // 检查结果是否为 undefined 并且参数是否存在于 KKK if (result === undefined && argumentsList in KKK) { return KKK[argumentsList]; } // 返回原始函数调用的结果 return result; } }; `
代码详细解释如下:
target
:被代理的目标函数,即 F3
;
thisArg
:如果原始函数是作为对象方法调用的,那么它的 this
指向;
argumentsList
:函数调用的参数列表;
内部先调用目标函数并获取其返回值。如果返回值为 undefined
且参数列表存在于 KKK
中,返回对应的映射值,否则返回原始返回值;
通过 Proxy
包装 F3
:javascript F3 = new Proxy(F3, kk_handler);
使用 Proxy
构造函数创建新的代理对象 F3
,将 F3
和处理器 kk_handler关联起来。
最终就可以拦截解密函数,实现完整的解密,然后就同方法 1 调用加密函数即可完成 type_128 的加密,解决方法有很多种,遇到问题阅读相关 API 即可解决相关的问题,至此整个 type_128 参数就分析完毕了。
往期推荐
[
Pydantic:目前最流行的Python数据验证库
[
python函数参数定义中的这两个分隔符,还有人不知道吗?
点个在看你最好看