某音网页端 X-Bogus 参数

news2024/11/17 16:34:05

逆向目标
目标:某音网页端用户信息接口 X-Bogus 参数

接口:aHR0cHM6Ly93d3cuZG91eWluLmNvbS9hd2VtZS92MS93ZWIvdXNlci9wcm9maWxlL290aGVyLw==

什么是 JSVMP?
JSVMP 全称 Virtual Machine based code Protection for JavaScript,即 JS 代码虚拟化保护方案。

JSVMP 的概念最早应该是由西北大学2015级硕士研究生匡开圆,在其2018年的学位论文中提出的,论文标题为:《基于 WebAssembly 的 JavaScript 代码虚拟化保护方法研究与实现》,同年还申请了国家专利,专利名称:《一种基于前端字节码技术的 JavaScript 虚拟化保护方法》,网上可以直接搜到.
抓包情况

随便来到某个博主主页,抓包后搜索可发现一个接口,返回的是 JSON 数据,里面包含了博主某音号,认证信息、签名,关注、粉丝、获赞等,请求 Query String Parameters 里包含了一个 X-Bogus 参数,每次请求会改变,此外还有 sec_user_id 是博主主页 URL 后面那一串,webid 直接请求主页返回内容里就有,msToken 与 cookie 有关,清除 cookie 访问,就没这个参数了,实测该接口不验证 webid 和 msToken,直接置空即可。

逆向分析
这条请求是 XHR 请求,所以直接下个 XHR 断点,当 URL 中包含 X-Bogus 参数时就断下:

往前跟栈,来到一个叫 webmssdk.js 的 JS 文件,这里就是生成参数的主要 JS 逻辑了,也就是 JSVMP,整体上做了一个混淆,这里可以使用 AST 来解混淆,K哥以前同样也写过 AST 的文章,这里还原混淆不是重点,咱们直接使用 V 佬的插件 v_jstools[3] 来还原:

还原后使用浏览器的 Overrides 替换功能将 webmssdk.js 替换掉,往上跟栈,如下图所示,到 W 这里就已经生成了 X-Bogus 了,this.openArgs[1] 就是携带了 X-Bogus 的完整 URL,仔细观察这段代码,有很多三元表达式,当 M 的值为 15 时,就会走到这段逻辑,U 的值生成之后,有一个 S[C] = U 的操作。

再往上看代码,S 是一个数组,单步调试的话会发现代码会一直走这个 if-else 的逻辑,几乎每一步都有 S 数组的参与,不断往里面增删改查值,for 循环里面的 I 值,决定着后续 if 语句的走向,这里也就是插桩的关键所在,如下图所示:

插桩分析
大的 for 循环和 if-else 逻辑有两个地方,为了保证最后的日志更加详细完整,在这两个地方都下个日志断点(右键 Add logpoint),断点内容为:

"位置 1", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {if (value == window) {return undefined} return value})"位置 2", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {if (value == window) {return undefined} return value})

插桩输出 S 的时候为什么要写这么长一串呢?首先 JSON.stringify() 方法的作用是将 JavaScript 值转换为 JSON 字符串,基础语法是 JSON.stringify(value[, replacer [, space]]),如果不将其转换成 JSON,那么 S 的值,输出可能是这样的:[empty, Array(26), 1, Array(0)],你看不到 Array 数组里面具体的值,该方法有个可选参数 replacer,如果 replacer 为函数,则 JSON.stringify 将调用该函数,并传入每个成员的键和值,在函数中可以对成员进行处理,最后返回处理后的值,如果此函数返回 undefined,则排除该成员,举个例子:

var obj1 = {key1: 'value1', key2: 'value2'}function changeValue(key, value) { if (value == 'value2') { return '公众号:K哥爬虫' } return value}var obj2 = JSON.stringify(obj1, changeValue)console.log(obj2)// 输出:{"key1":"value1","key2":"公众号:K哥爬虫"}
上面的代码中 JSON.stringify 传入了一个函数,当 value 为 value2 的时候就将其替换成字符串 公众号:K哥爬虫,接下来我们演示一下当 value 为 window 时,会发生什么:

根据报错我们可以看到这里由于循环引用导致异常,要知道在插桩的时候,如果插桩内容有报错,就会导致不能正常输出日志,这样就会缺失一部分日志,这种情况我们就可以加个函数处理一下,让 value 为 window 的时候,JSON 处理的时候函数返回 undefined,排除该成员,其他成员正常输出,如下图所示:

以上就是日志断点为什么要这样写的原因,下好日志断点后,注意前面我们下的 XHR 断点不要取消,然后刷新网页,控制台就开始打印日志了,因为有很多 XHR 请求都包含了 X-Bogus,如果你 XHR 断点取消了,日志就会一直打印直到卡死。日志输出完毕后,大约有8千多条,搜索就能看到最后一条日志 X-Bogus 已经生成了:

28个字符生成逻辑
直接在打印的日志页面右键 save as..,将日志导出到本地进行分析。X-Bogus 由28个字符组成,现在要做的就是看 DFSzswVOAATANH89SMHZqF9WX7n6 这28个字符是怎么来的,在日志里搜索这个字符串,找到第一次出现的地方,观察一下可以发现,他是逐个字符依次生成的,如下图红框所示:

在上图中,第8511行,X-Bogus 字符串的下一个元素是 null,到了第8512行,就生成数字6了,那么在这两步之间就是数字6的生成逻辑,这个时候我们看第8511行的日志断点是 位置 2 索引I 16 索引A 738,那么我们回到原网页,在位置2,下一个条件断点(右键 Add conditional breakpoint),当 I == 16 && A == 738 && S[7] && S[7] == 21 时就断下。之所以要加 S[7] 是因为 索引I 16 索引A 738 的位置有很多,在日志里搜一下大概有40多个,多加个限制条件就可以缩小范围,当然有可能加了多个条件仍然有多个位置都满足,这就需要你细心观察了,通过断点断下的时候看看控制台前面输出的日志来判断是不是我们想要的位置。这也是一个小细节,一定要找准位置,千万别搞混了。(提示一下,像我这样下断点的话,一般情况下会断下两次,第二次是满足要求的)

(注意:本文描述的日志的多少行、断点的具体位置、变量的具体值,可能会有所变化,以你的实际情况为准,但思路是一样的)

刷新网页,断下之后开始单步跟,来到下图所示的地方:

到这里之后,就不要下一步了,再下一步有可能整个语句就执行完毕了,其中的细节你看不到,所以这里我们在控制台挨个输入看看:

可以看到实际上的逻辑就是返回指定位置的字符,y 的值就是 S[5],m 的值就是 S[4],经过多次调试发现 m 的值是固定的,M 就是 charAt() 方法,我们再看看我们本地的日志,S[5] 的值为 [20],charAt() 取值出来就是6,逻辑完全正确。

现在我们还需要知道这个20是怎么来的,继续往上看,找到20第一次出现的地方,在第8510行,那么我们就要使其在上一步断下,也就是第8509行,如下图所示:

第8509行的索引信息为 位置 2 索引I 47 索引A 730,同样的下条件断点观察怎么生成的:

可以看到逻辑是 S[5] & S[6],再看我们本地 S[5] = 5647508、S[6] = 63,5647508 & 63 = 20,逻辑正确,20就是这么来的。接下来又开始找 5647508 和 63 是怎么生成的,同样在生成的上一步,也就是8508行下个条件断点,这行的索引为 位置 2 索引I 72 索引A 726。

可以看到 63 是直接 q[A] 生成的,q 是一个大数组,A 就是索引为 726,q 这个大数组怎么来的先不用管,而 5647508 这个大数字,搜索一下,发现有很多,咱们也先放着,到这里咱们可以总结一下最后一个字符的生成步骤如下:

short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="q[726] = 635647508 & 63 = 20short_str.charAt(20) = '6'
然后接日志着往上看,看倒数第二个字母是怎么来的,方法也和前面演示的一样,不断往前下条件断点,这里就不再逐步演示了,当你找完四个数字后,就可以开始看 5647508 这个大数字怎么来的了,搜索这个数字,同样的找到第一次出现的地方,在其前一步下条件断点,步骤捋出来会发现有一个乱码字符串经过 charCodeAt() 操作,再加上一些位运算得到的,乱码字符串类似下图所示:

至于这个乱码字符串怎么来的,我们后面再讲,到这里先总结一下,首先我们的 X-Bogus = DFSz swVO AATA NH89 SMHZ qF9W X7n6,将其看成每四个为一组,之所以这么分组,是因为你经过分析后会发现,每一组的每一个字符生成流程都是一样的,这里以最后两组为例,流程大致如下:

short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="X-Bogus = DFSz swVO AATA NH89 SMHZ qF9W X7n6============== 第6组【qF9W】=============="\u0002-%.*yê^s6V,".charCodeAt(15) = 158q[342] = 16158 << 16 = 10354688"\u0002-%.*yê^s6V,".charCodeAt(16) = 253q[408] = 8253 << 8 = 6476810354688 | 64768 = 10419456"\u0002-%.*yê^s6V,".charCodeAt(17) = 156156 | 10419456 = 10419612q[520] = 1651507210419612 & 16515072 = 10223616q[532] = 1810223616 >> 18 = 39short_str.charAt(39) = 'q'q[590]= 25804810419612 & 258048 = 192512q[602] = 12192512 >> 12 = 47short_str.charAt(47) = 'F'q[660] = 403210419612 & 4032 = 3456q[668] = 63456 >> 6 = 54short_str.charAt(54) = '9'q[726] = 6310419612 & 63 = 28short_str.charAt(28) = 'W'============== 第7组【X7n6】=============="\u0002-%.*yê^s6V,".charCodeAt(18) = 86q[342] = 1686 << 16 = 5636096"\u0002-%.*yê^s6V,".charCodeAt(19) = 44q[408] = 844 << 8 = 112645636096 | 11264 = 5647360"\u0002-%.*yê^s6V,".charCodeAt(20) = 148148 | 5647360 = 5647508q[520] = 165150725647508 & 16515072 = 5505024q[532] = 185505024 >> 18 = 21short_str.charAt(21) = 'X'q[590] = 2580485647508 & 258048 = 139264q[602] = 12139264 >> 12 = 34short_str.charAt(34) = '7'q[660] = 40325647508 & 4032 = 3200q[668] = 63200 >> 6 = 50short_str.charAt(50) = 'n'q[726] = 635647508 & 63 = 20short_str.charAt(20) = '6'
将流程对比一下就可以发现,每个步骤 q 里面的取值都是一样的,这个可以直接写死,不同之处就在于最开始的 charCodeAt() 操作,也就是返回乱码字符串指定位置字符的 Unicode 编码,第7组依次是 18、19、20,第6组依次是15、16、17,以此类推,第1组刚好是0、1、2,如下图所示:

每一组的逻辑都是一样的,我们就可以写个通用方法,依次生成七组字符串,最后拼接成完整的 X-Bogus,代码如下:(乱码字符串的生成后文会讲)

function getXBogus(originalString){ // 生成乱码字符串 var garbledString = getGarbledString(originalString); var XBogus = ""; // 依次生成七组字符串 for (var i = 0; i <= 20; i += 3) { var charCodeAtNum0 = garbledString.charCodeAt(i); var charCodeAtNum1 = garbledString.charCodeAt(i + 1); var charCodeAtNum2 = garbledString.charCodeAt(i + 2); var baseNum = charCodeAtNum2 | charCodeAtNum1 << 8 | charCodeAtNum0 << 16; // 依次生成四个字符 var str1 = short_str[(baseNum & 16515072) >> 18]; var str2 = short_str[(baseNum & 258048) >> 12]; var str3 = short_str[(baseNum & 4032) >> 6]; var str4 = short_str[baseNum & 63]; XBogus += str1 + str2 + str3 + str4; } return XBogus;}
乱码字符串生成逻辑
在进行下一步之前,我们要注意两点:

文章演示有些变量前后不对应,因为每次插桩的值都是会变的,看流程就行了,流程是正确的;

我们日志输出是经过 JSON.stringify 处理了的,有些步骤是向某个函数传入乱码字符串进行处理,你会发现处理后的结果和日志不一致,这是正常的。

乱码字符串的生成相对来说稍微复杂一点,但思路仍然一样,这里就不一一截图展示了,直接用日志描述一下关键步骤,注意以下日志是正向的步骤,就不逆着推了,建议自己先逆着把流程走一走,再来看这个步骤就看得懂了。

Step1:首先对 URL 后面的参数,也就是 Query String Parameters 进行两次 MD5、两次转 Uint8Array 处理,最后得到的 Uint8Array 对象在后面的步骤中用得到,步骤如下:

位置 1 索引I 4 索引A 134:将 URL 后面的参数进行 MD5 加密得到字符串位置 1 索引I 16 索引A 460:将上一步的字符串转换为 Uint8Array 对象位置 1 索引I 4 索引A 134:将上一步的 Uint8Array 对象进行 MD5 加密,得到字符串位置 1 索引I 29 索引A 472:将上一步的字符串转换为 Uint8Array 对象
上述步骤中,我们将最终得到的结果命名为 uint8Array,关键代码实现如下:

var md5 = require("md5");// 字符串转换为 Uint8Array 对象,缺失的变量自行补齐_0x5960a2 = function(a) { for (var c = a.length >> 1, e = c << 1, b = new Uint8Array(c), d = 0, f = 0; f < e; ) { b[d++] = _0x511f86[a.charCodeAt(f++)] << 4 | _0x511f86[a.charCodeAt(f++)]; } return b;}// originalString: URL 后面的原始参数var uint8Array = _0x5960a2(md5(_0x5960a2(md5(originalString))));
Step2:生成两个大数,一个是时间戳,我们称之为 fixedString1,另一个调用某个方法生成,我们称之为 fixedString2。

fixedString1位置 1 索引I 43 索引A 806:1663385262240 / 1000 = 1663385262.24fixedString2位置 1 索引I 16 索引A 834:M.apply(null, []) = 536919696
上述步骤中,M 对应以下方法,缺失的方法自行补齐(其中 _0x229792 是创建 canvas):

function _0x2996f8() { try { return _0x4b3b53 || (_0xb55f3e.perf ? -1 : (_0x4b3b53 = _0x229792(3735928559), _0x4b3b53)); } catch (a) { return -1; }}
Step3:先后生成两个数组,我们称之为 array1、array2,array2 就是由 array1 的元素位置变换后得来的,严格来讲,array1 不是一个完整的数组,而是一个个数字,这一点可以在日志中体现出来,为了方便我们就直接将其视为一个数组,两个数组都有19个元素,步骤如下:

array1[0] 至 array1[3] 为定值array1[4]位置 1 索引I 25 索引A 946:uint8Array[14]array1[5]位置 1 索引I 25 索引A 970:uint8Array[15]array1[6] 至 array1[7] 为定值,8、9 与 ua 有关array1[10]位置 1 索引I 52 索引A 1090:fixedString1 >> 24 = 99位置 1 索引I 47 索引A 1098:99 & 255 = 99array1[11]位置 1 索引I 52 索引A 1122:fixedString1 >> 16 = 25417位置 1 索引I 47 索引A 1130:25417 & 255 = 73array1[12]位置 1 索引I 52 索引A 1154:fixedString1 >> 8 = 6506755位置 1 索引I 47 索引A 1162:6506755 & 255 = 3array1[13]位置 1 索引I 52 索引A 1186:fixedString1 >> 0 = 241位置 1 索引I 47 索引A 1194:241 & 255 = 241array1[14]位置 1 索引I 52 索引A 1218:fixedString2 >> 24 = 32位置 1 索引I 47 索引A 1226:32 & 255 = 32array1[15]位置 1 索引I 52 索引A 1250:fixedString2 >> 16 = 8192位置 1 索引I 47 索引A 1258:8192 & 255 = 0array1[16]位置 1 索引I 52 索引A 1282:fixedString2 >> 8 = 2097342位置 1 索引I 47 索引A 1290:2097342 & 255 = 190array1[17]位置 1 索引I 52 索引A 1314:fixedString2 >> 0 = 536919696位置 1 索引I 47 索引A 1322:536919696 & 255 = 144array1[18]位置 1 索引I 27 索引A 1352:array1.reduce(function(a, b) { return a ^ b; }); = 100array1 完整值如下位置 1 索引I 27 索引A 1538:64,1.00390625,1,8,9,185,69,63,74,125,99,73,3,241,32,0,190,144,100array2 由 array1 元素交换位置而来:array2 = [array1[0], array1[2], array1[4], array1[6], array1[8], array1[10], array1[12], array1[14], array1[16], array1[18], array1[1], array1[3], array1[5], array1[7], array1[9], array1[11], array1[13], array1[15], array1[17]]array2 完整值如下array2 = [64,1,9,69,74,99,3,32,190,100,1.00390625,8,185,63,125,73,241,0,144]
Step4:将 Step3 得到的 array2 经过转换得到乱码字符串,步骤如下:

位置 1 索引I 16 索引A 1706:_0x2f2740.apply(null, array2) = "@\u0000\u0001\u000eíxE?\u0016c%> \u0000ó"位置 1 索引I 16 索引A 1760:_0x46fa4c.apply(null, ["", "@\u0000\u0001\u000e\tE?J}cI\u0003 \u0000d"]) = "\u0002-%.*yê^s6V,"位置 1 索引I 16 索引A 1812:_0x2b6720.apply(null, [2, 255, "\u0002-%.*yê^s6V,"]) = "\u0002-%.*yê^s6V,"
其中用到的函数:

function _0x2f2740(a, c, e, b, d, f, t, n, o, i, r, _, x, u, s, l, v, h, g) { let w = new Uint8Array(19); return w[0] = a, w[1] = r, w[2] = c, w[3] = _, w[4] = e, w[5] = x, w[6] = b, w[7] = u, w[8] = d, w[9] = s, w[10] = f, w[11] = l, w[12] = t, w[13] = v, w[14] = n, w[15] = h, w[16] = o, w[17] = g, w[18] = i, String.fromCharCode.apply(null, w);}function _0x46fa4c(a, c) { let e, b = [], d = 0, f = ""; for (let a = 0; a < 256; a++) { b[a] = a; } for (let c = 0; c < 256; c++) { d = (d + b[c] + a.charCodeAt(c % a.length)) % 256, e = b[c], b[c] = b[d], b[d] = e; } let t = 0; d = 0; for (let a = 0; a < c.length; a++) { t = (t + 1) % 256, d = (d + b[t]) % 256, e = b[t], b[t] = b[d], b[d] = e, f += String.fromCharCode(c.charCodeAt(a) ^ b[(b[t] + b[d]) % 256]); } return f;}function _0x583250(a) { return String.fromCharCode(a);}function _0x2b6720(a, c, e) { return _0x583250(a) + _0x583250(c) + e;}
自此,整个流程就走完了。可以用 JavaScript 来实现整个算法,用 Python 也可以。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1032798.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

根据条件关闭软件

使用下载工具时&#xff0c;经常出现磁盘空间已满&#xff0c;无法下载的情况。 使用shell写一个监控&#xff0c;每2分钟执行一次。判断当前磁盘的空间&#xff0c;低于2G时&#xff0c;关闭下载软件。 获取空间大小 ➜ ~ df -h …

如何运用API接口获取淘宝1688京东商品数据:从入门到实践

一、引言 随着电子商务的飞速发展&#xff0c;许多电商平台提供了API接口&#xff0c;允许开发者获取商品数据&#xff0c;以创建各种创新的应用。本文将详细介绍如何使用API接口获取商品数据&#xff0c;并通过代码示例进行演示。 二、API接口概述 1.API接口定义 API&…

【校招VIP】数据库理论之数据库范式

考点介绍&#xff1a; 范式是指关系数据库中的一种数据结构设计规范&#xff0c;用于规范关系型数据库中数据的存储方式&#xff0c;目的是为了消除冗余数据&#xff0c;减少数据的重复性&#xff0c;提高数据的一致性、完整性和正确性&#xff0c;避免数据的不一致性和冲突 …

三相组合式过电压保护器试验

三相组合式过电压保护器试验 试验目的 三相组合式过电压保护器主要分为有带串联间隙过压保护器和无间隙过压保护器两大类&#xff0c;其试验项目内容要求分别使用高压工频交流和高压直流电源。 三相组合式过电压保护器试验&#xff0c;主要是为了及早发现设备内部绝缘受潮及…

华为云云耀云服务器L实例评测|云耀云服务器L实例部署Dashdot服务器仪表盘

华为云云耀云服务器L实例评测&#xff5c;云耀云服务器L实例部署Dashdot服务器仪表盘 一、云耀云服务器L实例介绍二、Dashdot介绍2.1 Dashdot简介2.2 开发环境要求2.3 Yarn介绍 三、本次实践介绍3.1 本次实践简介3.2 本次环境规划 四、检查服务器环境4.1 购买云耀云服务器L实例…

Android中的缓存策略:LruCache和DiskLruCache

Android中的缓存策略&#xff1a;LruCache和DiskLruCache 导言 本篇文章主要是介绍Android中内置的两个缓存类的原理。所谓缓存&#xff0c;就是将获取的数据保存下来以便下次继续使用&#xff0c;这种技术尤其在网络请求和图片加载中有用&#xff0c;可以显著地提升App的性能…

SSM - Springboot - MyBatis-Plus 全栈体系(十三)

第三章 MyBatis 一、MyBatis 简介 1. 简介 MyBatis 最初是 Apache 的一个开源项目 iBatis, 2010 年 6 月这个项目由 Apache Software Foundation 迁移到了 Google Code。随着开发团队转投 Google Code 旗下&#xff0c; iBatis3.x 正式更名为 MyBatis。代码于 2013 年 11 月迁…

大模型训练显存优化推理加速方案

当前的深度学习框架大都采用的都是fp32来进行权重参数的存储&#xff0c;比如Python float的类型为双精度浮点数fp64&#xff0c;pytorch Tensor的默认类型为单精度浮点数fp32。随着模型越来越大&#xff0c;加速训练模型的需求就产生了。在深度学习模型中使用fp32主要存在几个…

R语言贝叶斯MCMC:GLM逻辑回归、Rstan线性回归、Metropolis Hastings与Gibbs采样算法实例...

原文链接&#xff1a;http://tecdat.cn/?p23236 在频率学派中&#xff0c;观察样本是随机的&#xff0c;而参数是固定的、未知的数量&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频 什么是频率学派&#xff1f; 概率被解释为一个随机过程的许多观测…

Spark SQL【电商购买数据分析】

Spark 数据分析 &#xff08;Scala&#xff09; import org.apache.spark.rdd.RDD import org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.{SparkConf, SparkContext}import java.io.{File, PrintWriter}object Taobao {case class Info(userId: Lo…

最该考的高含金量计算机证书盘点(文末领资料)

谈到大学规划&#xff0c;不少过来人都会建议萌新们在课余时间多多考证&#xff0c;俗话说的好“证多不压身”&#xff0c;今天我们就来聊一聊&#xff0c;计算机相关专业的大学生&#xff0c;有哪些证书可以考&#xff1f; 首先&#xff0c;不得不提的就是全国计算机二级考试…

web:[ACTF2020 新生赛]Exec

背景知识 命令执行漏洞 linux命令 题目 打开题目&#xff0c;页面显示的是一个ping 尝试一下 查看源代码发现 尝试ping一下百度 由题目名可知这道题关于exec&#xff08;命令执行&#xff09;&#xff0c;这里需要联想到可以多条命令执行 输入baidu.com;ls 尝试;号是否能够…

从统计语言模型到预训练语言模型---预训练语言模型(Transformer)

预训练模型的概念在计算机视觉领域并不陌生&#xff0c; 通常我们可以在大规模图像数据集上预先训练出一个通用 模型&#xff0c; 之后再迁移到类似的具体任务上去&#xff0c; 这样在减少对图像样本需求的同时&#xff0c; 也加速了模型的开发速度。计 算机视觉领域采用 Image…

互联网医院系统|互联网医院软件功能与广阔应用领域

随着科技的不断进步和人们对健康需求的提高&#xff0c;互联网医院已经成为当今医疗领域的热点话题。作为一种融合了互联网和医疗服务的创新模式&#xff0c;互联网医院带来了许多便利和改变。本文将详细介绍互联网医院的软件功能、应用范围以及未来的发展趋势。 互联网医院通过…

【计算机毕业设计】基于SpringBoot+Vue电影在线订票系统的开发与实现

博主主页&#xff1a;一季春秋博主简介&#xff1a;专注Java技术领域和毕业设计项目实战、Java、微信小程序、安卓等技术开发&#xff0c;远程调试部署、代码讲解、文档指导、ppt制作等技术指导。主要内容&#xff1a;毕业设计(Java项目、小程序等)、简历模板、学习资料、面试题…

机器学习笔记:概念对比——损失函数,代价函数,目标函数

损失函数 Loss Function 通常是针对单个训练样本而言 给定一个模型输出 和一个真实值y &#xff0c;损失函数是 代价函数 Cost Function 通常是针对整个训练集&#xff08;或者在使用 mini-batch gradient descent 时一个 mini-batch&#xff09;的总损失 目标函数 Objec…

备考cisp拿证,收藏这一篇就够了

为什么要考CISP 认证机构&#xff1a;中国信息安全测评中心&#xff0c;是中央批准成立的国家权威信息安全测评机构&#xff0c;CISP是当之无愧的国家级认证&#xff0c;是国内对信息安全从业人员资质能力的最高认可。 持证人数&#xff1a;在信息安全行业&#xff0c;持有CI…

多维数据可视化技术,Radviz可视化原理,向量化的 Radviz(vectorized Radviz,简称 VRV)

目录 多维数据可视化技术 Radviz可视化原理 向量化的 Radviz(vectorized Radviz,简称 VRV) 多维数据可视化技术 多维和高维数据普遍存在于我们的日常生活和科学研究中 . 比如 , 手机就包括品牌、型号、尺寸、重量、 生产日期、屏幕尺寸和电池容量等几十个属性; 又如 , 生物…

Pygame中Sprite类的使用3

在Pygame中Sprite类的使用2_棉猴的博客-CSDN博客中提到了通过派生自pygame.sprite.Sprite类的自定义类Zombie&#xff0c;可以实现一个僵尸的移动。可以通过pygame.sprite.Group类实现对多个Zombie类实例的管理&#xff0c;即可以实现多个僵尸的移动。 1 pygame.sprite.Group类…

一文彻底理解synchronized(通俗易懂的synchronized)

目录 一、什么是synchronized 二、synchronized的四种用法 2.1、修饰一个代码块 2.2、修饰一个方法 2.3、修饰一个静态的方法 2.4、修饰一个类 三、使用案例分析 3.1、修饰一个代码块 3.2、修饰一个方法 3.3、修饰一个静态的方法 3.4、修饰一个类 3.5 经典用法&…