长亭百川云 - 文章详情

最新雪王 type__1286 参数逆向分析,带你免费喝一杯~

K小哥

71

2024-08-06

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,联系作者立即删除!

前言

最近巴黎奥运会,很多平台都搞起了免费饮品免单的活动,当然雪王也不例外,小程序与 App 同为 webview 类型的程序,起初该平台只有一个 sign 加密,最近压力上来了,更新了一个新参数 type_1286 ,不少粉丝也被这个参数难住了,本文仅对雪王的这个新参数进行逆向分析,仅供学习交流**。**

逆向目标

  • 某雪冰城小程序、某雪冰城 App

  • I+Wwj+eoi+W6jzovL+icnOmbquWGsOWfji9hb1RwNU1zT0tJMHRHWWM=

构建调试框架

APP 端调试 webview

APP 端的 webview 调试主要借助 XP 模块,或者 LSP。然后导入 webview 模块即可调试, 以 LSP 为例,模拟器装好面具以及 LSP 后,将 webview 模块导入并且选择指定 APP:

然后重启模拟器,在浏览器输入chrome://inspect/#devices ,若无设备加载出来,则在 cmd 控制台多输入几次 adb devices 直到设备加载出来:

然后点击 inspect 即可进入,出现正常的 F12 界面证明调试成功:

小程序端调试 webview

  • 首先 USB 数据线连接手机进入调试模式;

  • 首先微信访问 http://debugxweb.qq.com/?inspector=true 确定是否可以用(能打开就能用);

  • 微信上打开你需要调试的页面;

  • 谷歌浏览器地址栏输入 chrome://inspect/#devices 等待一会儿 (浏览器需要具备翻强功能);

  • 点击对应网页或者小程序 inspect 即可出现调试栏,然后像正常调试页面即可chrome://inspect/#devices

最后效果如 App 端开启调试的结果相同。

抓包分析

进入免单界面,点击领取,在开发者工具中即可查看到该接口,如下:

其主要是加密参数为 url 接口中的 type_1286,以及提交内容中的 sign,本文将重点讲 type_1286 参数的生成。

逆向分析

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 行左右,下面我们用算法还原的方式将整个算法进行剖析。

算法还原 1

我们还是回到加密函数 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 大数组所占的行数,整个算法也就几百行左右,比起之前的代码可谓是相当整洁。

算法还原 2

刚刚我们提到在扣解密函数的时候,可能会由于时机不正确,导致复制下来的 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 包装 F3javascript F3 = new Proxy(F3, kk_handler); 使用 Proxy 构造函数创建新的代理对象 F3,将 F3 和处理器 kk_handler关联起来。

最终就可以拦截解密函数,实现完整的解密,然后就同方法 1 调用加密函数即可完成 type_128 的加密,解决方法有很多种,遇到问题阅读相关 API 即可解决相关的问题,至此整个 type_128 参数就分析完毕了。

往期推荐

[

Pydantic:目前最流行的Python数据验证库

](https://mp.weixin.qq.com/s?__biz=MzU1Mjc3MzczMQ==&mid=2247492750&idx=1&sn=c4c81398c59c792b50a904f1f2c1cbec&chksm=fbfe5582cc89dc942ae741a1bc3ce86b52b928431f7e887098acca018cc2606fcbca70e7a1cc&scene=21#wechat_redirect)

[

python函数参数定义中的这两个分隔符,还有人不知道吗?

](https://mp.weixin.qq.com/s?__biz=MzU1Mjc3MzczMQ==&mid=2247492687&idx=1&sn=23b6468c94548b4835dd6bb2e834e388&chksm=fbfe5543cc89dc55905182b1c184480ab20bda6f3519bc8f991f9cdafb58a68b5738ee2a16fc&scene=21#wechat_redirect)

点个在看你最好看

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2