【JS 逆向百例】X-Bogus 逆向分析,JSVMP 纯算法还原

news2024/11/18 3:22:04

声明

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

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


文章目录

    • 声明
    • 逆向目标
    • 什么是 JSVMP?
    • JSVMP 有哪些学习资料?
    • JSVMP 逆向方法有哪些?
    • 抓包情况
    • 逆向分析
    • 插桩分析
    • 28个字符生成逻辑
    • 乱码字符串生成逻辑


逆向目标

  • 目标:某音网页端用户信息接口 X-Bogus 参数
  • 接口:aHR0cHM6Ly93d3cuZG91eWluLmNvbS9hd2VtZS92MS93ZWIvdXNlci9wcm9maWxlL290aGVyLw==

什么是 JSVMP?

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

JSVMP 的概念最早应该是由西北大学2015级硕士研究生匡开圆,在其2018年的学位论文中提出的,论文标题为:《基于 WebAssembly 的 JavaScript 代码虚拟化保护方法研究与实现》,同年还申请了国家专利,专利名称:《一种基于前端字节码技术的 JavaScript 虚拟化保护方法》,网上可以直接搜到,也可在公众号【K哥爬虫】后台回复 JSVMP,免费获取原版高清无水印的论文和专利。本文就简单介绍一下 JSVMP,想要详细了解,当然还是建议去读一下这篇论文。

01

JSVMP 的核心是在 JavaScript 代码保护过程中引入代码虚拟化思想,实现源代码的虚拟化过程,将目标代码转换成自定义的字节码,这些字节码只有特殊的解释器才能识别,隐藏目标代码的关键逻辑。在匡开圆的论文中,利用 WebAssembly 技术实现了特殊的虚拟解释器,通过编译隐藏解释器的执行逻辑。JSVMP 的保护流程如下图所示:

02

一个完整的 JSVMP 保护系统,大致的架构应该是这样子的:服务器端读取 JavaScript 代码 —> 词法分析 —> 语法分析 —> 生成AST语法树 —> 生成私有指令 —> 生成对应私有解释器,将私有指令加密与私有解释器发送给浏览器,然后一边解释,一边执行。

03

JSVMP 有哪些学习资料?

除了匡开圆的论文以外,还有以下文章也值得学习:

  • H5应用加固防破解-js虚拟机保护方案浅谈
  • JS加密?用虚拟机opcode保护JS源码
  • 给"某音"的js虚拟机写一个编译器

JSVMP 逆向方法有哪些?

就目前来讲,JSVMP 的逆向方法有三种(自动化不算):RPC 远程调用,补环境,日志断点还原算法,其中日志断点也称为插桩,找到关键位置,输出关键参数的日志信息,从结果往上倒推生成逻辑,以达到算法还原的目的,RPC 技术K哥以前写过文章,补环境的方式以后有时间再写,本文主要介绍如何使用插桩来还原算法。

抓包情况

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

04

05

逆向分析

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

06

07

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

08

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

09

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

10

插桩分析

大的 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})

11

插桩输出 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 传入了一个函数,当 valuevalue2 的时候就将其替换成字符串 公众号:K哥爬虫,接下来我们演示一下当 valuewindow 时,会发生什么:

12

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

13

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

14

28个字符生成逻辑

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

15

在上图中,第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多个,多加个限制条件就可以缩小范围,当然有可能加了多个条件仍然有多个位置都满足,这就需要你细心观察了,通过断点断下的时候看看控制台前面输出的日志来判断是不是我们想要的位置。这也是一个小细节,一定要找准位置,千万别搞混了。(提示一下,像我这样下断点的话,一般情况下会断下两次,第二次是满足要求的)

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

16

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

17

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

18

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

19

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

20

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

21

22

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

23

24

25

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

short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="

q[726] = 63
5647508 & 63 = 20
short_str.charAt(20) = '6'

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

26

至于这个乱码字符串怎么来的,我们后面再讲,到这里先总结一下,首先我们的 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ê^s6ðýÇžýœV,”".charCodeAt(15) = 158
q[342] = 16
158 << 16 = 10354688
"\u0002ÿ-%.*yê^s6ðýÇžýœV,”".charCodeAt(16) = 253
q[408] = 8
253 << 8 = 64768
10354688 | 64768 = 10419456
"\u0002ÿ-%.*yê^s6ðýÇžýœV,”".charCodeAt(17) = 156
156 | 10419456 = 10419612

q[520] = 16515072
10419612 & 16515072 = 10223616
q[532] = 18
10223616 >> 18 = 39
short_str.charAt(39) = 'q'

q[590]= 258048
10419612 & 258048 = 192512
q[602] = 12
192512 >> 12 = 47
short_str.charAt(47) = 'F'

q[660] = 4032
10419612 & 4032 = 3456
q[668] = 6
3456 >> 6 = 54
short_str.charAt(54) = '9'

q[726] = 63
10419612 & 63 = 28
short_str.charAt(28) = 'W'

============== 第7组【X7n6】==============

"\u0002ÿ-%.*yê^s6ðýÇžýœV,”".charCodeAt(18) = 86
q[342] = 16
86 << 16 = 5636096
"\u0002ÿ-%.*yê^s6ðýÇžýœV,”".charCodeAt(19) = 44
q[408] = 8
44 << 8 = 11264
5636096 | 11264 = 5647360
"\u0002ÿ-%.*yê^s6ðýÇžýœV,”".charCodeAt(20) = 148
148 | 5647360 = 5647508

q[520] = 16515072
5647508 & 16515072 = 5505024
q[532] = 18
5505024 >> 18 = 21
short_str.charAt(21) = 'X'

q[590] = 258048
5647508 & 258048 = 139264
q[602] = 12
139264 >> 12 = 34
short_str.charAt(34) = '7'

q[660] = 4032
5647508 & 4032 = 3200
q[668] = 6
3200 >> 6 = 50
short_str.charAt(50) = 'n'

q[726] = 63
5647508 & 63 = 20
short_str.charAt(20) = '6'

将流程对比一下就可以发现,每个步骤 q 里面的取值都是一样的,这个可以直接写死,不同之处就在于最开始的 charCodeAt() 操作,也就是返回乱码字符串指定位置字符的 Unicode 编码,第7组依次是 18、19、20,第6组依次是15、16、17,以此类推,第1组刚好是0、1、2,如下图所示:

27

每一组的逻辑都是一样的,我们就可以写个通用方法,依次生成七组字符串,最后拼接成完整的 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.24

fixedString2
位置 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:先后生成两个数组,我们称之为 array1array2array2 就是由 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 = 99

array1[11]
位置 1 索引I 52 索引A 1122:fixedString1 >> 16 = 25417
位置 1 索引I 47 索引A 1130:25417 & 255 = 73

array1[12]
位置 1 索引I 52 索引A 1154:fixedString1 >> 8 = 6506755
位置 1 索引I 47 索引A 1162:6506755 & 255 = 3

array1[13]
位置 1 索引I 52 索引A 1186:fixedString1 >> 0 = 241
位置 1 索引I 47 索引A 1194:241 & 255 = 241

array1[14]
位置 1 索引I 52 索引A 1218:fixedString2 >> 24 = 32
位置 1 索引I 47 索引A 1226:32 & 255 = 32

array1[15]
位置 1 索引I 52 索引A 1250:fixedString2 >> 16 = 8192
位置 1 索引I 47 索引A 1258:8192 & 255 = 0

array1[16]
位置 1 索引I 52 索引A 1282:fixedString2 >> 8 = 2097342
位置 1 索引I 47 索引A 1290:2097342 & 255 = 190

array1[17]
位置 1 索引I 52 索引A 1314:fixedString2 >> 0 = 536919696
位置 1 索引I 47 索引A 1322:536919696 & 255 = 144

array1[18]
位置 1 索引I 27 索引A 1352:array1.reduce(function(a, b) { return a ^ b; }); = 100

array1 完整值如下
位置 1 索引I 27 索引A 1538:64,1.00390625,1,8,9,185,69,63,74,125,99,73,3,241,32,0,190,144,100

array2 由 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\t¹E?J}cI\u0003ñ \u0000¾d"]) = "\u0002ÿ-%.*yê^s6ðýÇžýœV,”"

位置 1 索引I 16 索引A 1812:
_0x2b6720.apply(null, [2, 255, "\u0002ÿ-%.*yê^s6ðýÇžýœV,”"]) = "\u0002ÿ-%.*yê^s6ðýÇžýœV,”"

其中用到的函数:

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 也可以,完善代码后随便请求一个博主主页,简单解析几个数据,输出正常:

28

29

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

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

相关文章

72小时灵感冲刺,创意就该这么玩 | LigaAI Hackathon特别策划

2023 年 1 月 9 日至 12 日&#xff0c;LigaAI 团队全员出逃&#xff1a;放下迭代&#xff0c;暂缓需求&#xff0c;处处充斥着「可以摸鱼➕卷死他们」的矛盾又欢乐的气息。职场和远程的伙伴们无一不在热烈讨论、积极组队、抢占会议室、搜刮零食饮料…… 是什么让矜持内敛的技…

微信小程序的启动和渲染过程(加组件分类和组件的基本使用以及API分类)

小程序的启动过程 把小程序的代码包下载到本地解析app.json全局配置文件执行app.js小程序入口文件,调用App()创建小程序实例渲染小程序首页小程序启动完成 小程序页面渲染的过程 加载解析页面的.json配置文件加载页面的.wxml模板和.wxss样式执行页面的.js文件,调用page(创建…

网站建设的具体流程

有网站制作需求的朋友想了解一下网站建设的具体流程&#xff0c;防止不良的网站制作公司的业务人员用虚假的工作量欺骗自己报出高价&#xff0c;米贸搜详细给你整理一下一个网站的建设有哪些流程:1.在线或当面沟通网站制作需求&#xff0c;详细沟通网站的功能需求、设计风格以及…

功率放大模块的作用是什么(功率放大器模块的应用范围)

功率放大模块是一种能够把开关电源、数字功放集成到一起的放大模块。很多人对于功率放大模块的作用是什么以及功率放大模的应用范围都不清楚&#xff0c;下面就来详细的为大家介绍下什么是功率放大模块以及功率放大模块的基础知识。 一、什么是功率放大模块 功率放大模块是一种…

谷歌AR应用挑战赛上那些富有创意的AR项目集锦

前不久&#xff0c;谷歌面向全球100多个国家和地区的开发者们推出ARCore Geospatial API挑战赛&#xff0c;获奖的AR应用可得到1000美元到1.2万美元的奖金&#xff0c;支持多种不同的内容类别&#xff0c;比如AR导航、AR游戏、AR娱乐等等。据悉&#xff0c;Geospatial API是谷歌…

贵阳某小区一次HC小区管理系统自研道闸故障解决记录

一次HC小区管理系统自研道闸故障解决记录&#xff0c;方便其他小伙伴出现问题的时候提供解决思路 早上九点钟 客户说道闸用不了&#xff0c;这个客户不是物业&#xff0c;而是科技公司&#xff0c;他们给物业安装的道闸。 问题描述是&#xff0c;mqtt 也重启了&#xff0c;物…

Allegro如何打开锁定走线线宽的功能操作指导

Allegro如何打开锁定走线线宽的功能操作指导 在做PCB设计的时候,有时需要用到锁定线宽的功能,让走线一直保持固定的线宽不变化,例如下图 任何网络的走线在任何地方都是固定20mil的线宽 具体操作如下 选择菜单的Setup选择User Preferences

解决IE页面打不开及白屏问题

IE浏览器控制台报错如下&#xff1a; 解决办法&#xff1a; ① public文件下新建TextEncoder.js /* eslint-disable */ ; var textEncoder (function(window) {if (undefined ! window.TextEncoder) { return }function _TextEncoder() {// --DO NOTHING}_TextEncoder.prot…

steam搬砖项目详细介绍

一、项目介绍 其实&#xff0c;Steam就是一个全球的游戏平台&#xff0c;搬砖主要是搬的一款火遍全球的游戏CSGO的装备和饰品。CS听说过吧&#xff0c;这款游戏就是CS的一个系列。&#xff08;通俗易懂的理解就是&#xff0c;从国外steam游戏平台购买装备&#xff0c;再挂到国内…

分销商城小程序开发

武汉微驱动科技有限公司 目前电商行业正在蓬勃发展&#xff0c;越来越多的商家想要利用各种社交关系&#xff0c;用社交软件扩大经营规模&#xff0c;小程序分销是最近很火爆的流行趋势&#xff0c;不过有不少商家还不清楚“什么是小程序分销模式&#xff1f;小程序分销商城是什…

gerapy部署项目报错:ModuleNotFoundError: No module named ...

使用gerapy部署我的项目&#xff0c;rebuild的时候是成功的&#xff0c;但是deploy的时候失败了&#xff0c;报错&#xff1a; Client 1 Failed to Deploy 没有显示具体的错误&#xff0c;只能到gerapy的部署目录找日志。 根据 ll 命令&#xff0c;找到了最新的日志文件&…

AutoSAR Crypto_UtilizationOfCryptoServices-AUTOSAR加密服务使用

1 Stack Architecture The Crypto Service Manager (CSM) CSM控制一个或者多个Client对一个或者多个同步或异步加密服务。它提供了优先级队列来管理专用CRYPTO不能直接处理的作业 CSM的功能如下&#xff1a; ● HASH计算&#xff1b; ● 消息认证码的生成和校验&#xff1b; ●…

Centos6源码安装Haproxy进行四层代理

一.背景 公司使用专线与第三方公司进行系统交互&#xff0c;给定了我们业务IP的使用范围&#xff0c;防火墙策略只开放业务IP范围之内的IP地址才能访问&#xff0c;如果源IP不在业务IP范围之内&#xff0c;那么通过互联IP过去是访问不了的。我们的做法是为了不影响现有业务&…

完整复现YOLOv8:包括训练、测试、评估、预测阶段【本文源码已开源,地址在文章末尾】

训练过程展示: 目录 1、复现过程1.1、配置开发环境1.2、demo预测实现过程2 、项目实现方法与代码(包括训练、测试、评估、预测阶段)2.1、训练、测试、评估、预测代码适配2.2、同时开始训练、测试、评估、预测2.3、训练完之后进行预测2.4、训练、评估、混淆矩阵、召回曲线等…

Linux 基础常用命令 整理

Shell Shell 这个单词的原意是“外壳”&#xff0c;跟 kernel&#xff08;内核&#xff09;相对应&#xff0c;比喻内核外面的一层&#xff0c;即用户跟内核交互的对话界面。 Shell 是一个程序&#xff0c;提供一个与用户对话的环境。这个环境只有一个命令提示符&#xff0c;让…

解读小红书:零食行业用户洞察报告

随着消费持续升级、零食正作为休闲生活的一部分开始走近更多用户。以日趋多元化的消费需求为导向&#xff0c;零食行业用户又透露出哪些消费特征呢&#xff1f; 本期为大家解读小红书官方发布的《「灵感补给站」小红书2023年零食行业用户洞察报告》&#xff0c;基于上千名用户定…

从状态机的角度async和await的实现原理

一. 深度剖析准备&#xff1a;先给VS安装一个插件ILSpy,这样更容易反编译代码进行查看,另外要注意反编译async和await的时候,要把C#代码版本改为4.0哦。1.什么是状态机(1).含义&#xff1a;通常我们所说的状态机(State Machine)指的是有限状态自动机的简称&#xff0c;是现实事…

6.5、文件传送协议FTP

将某台计算机中的文件通过网络传送到可能相距很远的另一台计算机中&#xff0c;是一项基本的网络应用&#xff0c;即文件传送。 文件传送协议FTP\color{red}文件传送协议\texttt{FTP}文件传送协议FTP (File Transfer Protocol)是因特网上使用得最广泛的文件传送协议。 FTP 提供…

记录--Vue开发历程---音乐播放器

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 一、audio标签的使用 1、Audio 对象属性 2、对象方法 二、效果 效果如下&#xff1a; 三、代码 代码如下&#xff1a; MusicPlayer.vue <template><div class"music"><!-- 占…

【QFD】质量保证需求

QFD体系把前端商业策略成果和产品有效开发紧密连接起来工具。QFD是强调需求与功能的对应&#xff0c;验证确定的市场需求与产品功能设计的关联性&#xff0c;与功能之间&#xff0c;与需求之间的矛盾性&#xff0c;也叫质量屋。 1.什么是QFD 什么是QFD质量功能展开&#xff0…