开始 2021年3月12日,植树节,腾讯发起了极客技术挑战赛,规则很简单,在页面上只有一个种树按钮,点击一下,就种下了一棵树。树种得越多越好,排行榜会根据树的数量排名。
本期的比赛秉承了极客技术挑战赛的一贯传统,它非常简单,几乎不需要写代码,你只需要简单地点击最下方种树按钮就可以了,最终谁种的树最多,谁就是冠军! 比赛截止后,种下的树数量越多排名越靠前,如遇数量相同,则按照到达该数量的时间排名。
页面在此:码上种树 。
作为程序员,一定会本能的按下F12,打开开发者工具。一是查看点击按钮执行的代码,二是查看提交给服务器的数据包。 通过分析找到关键的js代码,
这段代码通过分析含义如下:
通过pull
请求获得数据字段a
和js文件名c
字段
动态加载这个js文件,然后将pull
请求返回的结果传递给js执行
将执行完的结果以及pull
请求返回的t
字段发送给push
请求
第一关 查看一下这个A274075A.js
文件源码,只有一行代码,原来就是延迟2秒将数组的第一个值返回:
1 window .A274075A=async function ({a} ) {return new Promise (_ =>setTimeout (__ => _(a[0 ]),2000 ))}
从抓包获取的2个请求来看,也验证了是这个逻辑没错,于是,马上构建了一个程序模拟提交这2个请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void Run ( ) { WebClient client = new WebClient(); client.Encoding = Encoding.UTF8; while (true ) { var result = client.DownloadString("http://159.75.70.9:8081/pull?u=XXXXXXXXXXXXXXXXXXXXXXXXXXX" ); Console.WriteLine(result); var pull = Newtonsoft.Json.JsonConvert.DeserializeObject<PULL>(result); if (pull == null || pull.t == null ) continue ; long val = pull.a[0 ]; result = client.DownloadString($"http://159.75.70.9:8081/push?t={pull.t} &a={val} " ); Console.WriteLine(result); var res = Newtonsoft.Json.JsonConvert.DeserializeObject<Result>(result); if (res.success != 1 ) break ; Console.WriteLine("总数:" + res.score); } }
程序跑起来,服务器不断提示成功,也在不断累积分数,看来分析的没错。但高兴别太早,当刷到1万分的时候服务器开始提示错误的答案了,看来并没有想象中那么简单。
值得一提的是,这里采用多线程并发请求是没有意义的,因为在成功完成一次push
请求之前获得的数据是一摸一样的,同时也只会记录一次得分,所以重复的请求没有意义,不得不说腾讯的工程师在处理高并发这块还是相当有经验的。
第二关 继续在页面上点击种树,页面上成功种下了一棵树,这时发现逻辑是没有变化的,只是第二步服务器返回的js文件发生了变化,查看一下源代码
1 window .A3C2EA99=async function ({a} ) {return new Promise (_ =>setTimeout (__ => _(a[0 ]*a[0 ]+a[0 ]),2000 ))}
原来只是增加了一点简单计算,那么只需稍微处理一下我们的逻辑即可,将原来的long val = pull.a[0];
修改为long val = pull.a[0] * pull.a[0] + pull.a[0];
。
程序又能执行了,顺利通过第二关,此时分数已经达到10万分。
第三关 根据前面的经验,服务器返回的js文件就是通关的关键,将第1个请求返回的数据字段,代入到函数中计算求得一个结果,将这个结果发到第2个请求,即完成了一次种树。所以,破解函数(分析函数的执行逻辑)即通关的钥匙。
然而接下来似乎没有那么简单了,查看一下这一关的js文件:
1 eval (atob("dmFyIF8weGU5MzY9WydBNTQ3Mzc4OCddOyhmdW5jdGlvbihfMHg0OGU4NWMsXzB4ZTkzNmQ4KXt2YXIgXzB4MjNmYzVhPWZ1bmN0aW9uKF8weDI4NThkOSl7d2hpbGUoLS1fMHgyODU4ZDkpe18weDQ4ZTg1Y1sncHVzaCddKF8weDQ4ZTg1Y1snc2hpZnQnXSgpKTt9fTtfMHgyM2ZjNWEoKytfMHhlOTM2ZDgpO30oXzB4ZTkzNiwweDE5NikpO3ZhciBfMHgyM2ZjPWZ1bmN0aW9uKF8weDQ4ZTg1YyxfMHhlOTM2ZDgpe18weDQ4ZTg1Yz1fMHg0OGU4NWMtMHgwO3ZhciBfMHgyM2ZjNWE9XzB4ZTkzNltfMHg0OGU4NWNdO3JldHVybiBfMHgyM2ZjNWE7fTt3aW5kb3dbXzB4MjNmYygnMHgwJyldPWZ1bmN0aW9uKF8weDMzNTQzNyl7dmFyIF8weDFhYWMwMj0weDMwZDNmO2Zvcih2YXIgXzB4M2JlZDZhPTB4MzBkM2Y7XzB4M2JlZDZhPjB4MDtfMHgzYmVkNmEtLSl7dmFyIF8weDM3NTM0MD0weDA7Zm9yKHZhciBfMHgxZGRiNzc9MHgwO18weDFkZGI3NzxfMHgzYmVkNmE7XzB4MWRkYjc3Kyspe18weDM3NTM0MCs9XzB4MzM1NDM3WydhJ11bMHgwXTt9XzB4Mzc1MzQwJV8weDMzNTQzN1snYSddWzB4Ml09PV8weDMzNTQzN1snYSddWzB4MV0mJl8weDNiZWQ2YTxfMHgxYWFjMDImJihfMHgxYWFjMDI9XzB4M2JlZDZhKTt9cmV0dXJuIF8weDFhYWMwMjt9Ow==" ))
很明显这是一段base64编码后的字符,js用eval
函数来执行脚本,那么我们先将这段base64转明文,得到了如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var _0xe936 = ['A5473788' ]; (function (_0x48e85c, _0xe936d8 ) { var _0x23fc5a = function (_0x2858d9 ) { while (--_0x2858d9) { _0x48e85c['push' ](_0x48e85c['shift' ]()); } }; _0x23fc5a(++_0xe936d8); }(_0xe936, 0x196 ));var _0x23fc = function (_0x48e85c, _0xe936d8 ) { _0x48e85c = _0x48e85c - 0x0 ; var _0x23fc5a = _0xe936[_0x48e85c]; return _0x23fc5a; };window [_0x23fc('0x0' )] = function (_0x335437 ) { var _0x1aac02 = 0x30d3f ; for (var _0x3bed6a = 0x30d3f ; _0x3bed6a > 0x0 ; _0x3bed6a--) { var _0x375340 = 0x0 ; for (var _0x1ddb77 = 0x0 ; _0x1ddb77 < _0x3bed6a; _0x1ddb77++) { _0x375340 += _0x335437['a' ][0x0 ]; } _0x375340 % _0x335437['a' ][0x2 ] == _0x335437['a' ][0x1 ] && _0x3bed6a < _0x1aac02 && (_0x1aac02 = _0x3bed6a); } return _0x1aac02; };
这段代码是经过混淆的,并不太好分析,不过根据前面的经验,关键应该在于window[_0x23fc('0x0')]
这个函数,于是我简单的将这个函数翻译成C#
代码,然后让程序去执行好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static long Func (long [] arr ) { var res = 0x30d3f ; for (var i = 0x30d3f ; i > 0x0 ; i--) { long temp = 0x0 ; for (var j = 0x0 ; j < i; j++) { temp += arr[0x0 ]; } if (temp % arr[0x2 ] == arr[0x1 ] && i < res) { res = i; } } return res; }
再一次程序运算成功了,不过这个函数计算结果的速度实在是太慢了,得有好几秒才能完成一次请求,页面上点击也是同样如此,这说明翻译没错,问题出在函数本身,如果有采用模拟浏览器点击事件提交的朋友,估计要卡在这关了,心疼一波。 分析一下这段代码的逻辑,可以发现,原来就是不断累加a[0]
,然后模a[2]
,求取模结果等于a[1]
时,计数器i
的值。 简单总结一下,就是求a[0]
的多少倍能够使得模a[2]
正好等于a[1]
,这里的代码有个坑就是,求得了这个倍数之后,仍然会继续执行,直到求得最小能满足条件的倍数,既然是求最小倍数,那么这里让i
倒序循环就不合理,慢就慢在这个地方。 这个函数的真正用意是求a[0]
的最小 多少倍能够使得模a[2]
正好等于a[1]
,了解了这个,代码就不难修改了,优化后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static long Func3 (long [] arr ) { var res = 0x30d3f ; long temp = 0 ; for (var i = 1 ; i <= 0x30d3f ; i++) { temp += arr[0 ]; if (temp % arr[0x2 ] == arr[0x1 ]) { return i; } } return res; }
优化之后,速度大大提升,通过第三关,分数达到25万。
第四关 直接奔向js,得到了这样一段代码,立即傻眼。
这段代码简直就是天书,人是根本不可能看明白的。不过既然浏览器能识别,那还是让浏览器解析看看吧。 这里的[]
、!+[]
、!![]
在其他编程语言来说是无效的,不过对于js解析器是有意义的,简单代入几个值试试效果:
于是从这里开始入手,先把文件保存下来放在一个能识别js的编辑器里去查看,这会比白花花的记事本好看得多,比如我这里用的notepad++
这样就很容易找到2个匹配的括号,把括号内容截取出来放到浏览器去执行,
为了省事,这里尽可能找到更长的括号对,然后执行并替换,经过几轮操作这段代码就变成了这样:
1 2 3 4 5 6 7 8 9 10 11 window .A593C8B8 = async (_) => (($, _, __, ___, ____ ) => { let _____ = function * ( ) { while ([]) yield [(_, __ ) => _ + __, (_, __ ) => _ - __, (_, __ ) => _ * __][++__ % 3 ]["bind" ](+[], ___, ____) }(); let ______ = function (_____, ______, _______ ) { ____ = _____; ___ = ______["next" ]()["value" ](); __ == _["a" ]["length" ] && _______(-___) }; return new Promise (__ => _["a" ]["forEach" ](___ => $["setTimeout" ](____ => ______(___, _____, __), ___))) })(window , _, +[], +[], +[])
简单分析一下这段代码,第一个函数返回的是一个生成器,这个生成器会不断循环的返回3个函数,分别是两数相加 ,两数相减 ,两数相乘 。 第二个函数是枚举生成器,同时将a
数组对象的值代入执行,当执行完a
数组最后一个值后,将结果取负后返回。 最后,遍历a
数组对象,将值输入到第二个函数。 这里有个坑就是,将a
数组的值输入到函数时,是会经历setTimeout
延时的,而延时时间正好是这个值本身,也就是说较小的值会优先参与计算,这也是所谓的睡眠排序 法。 为了计算速度当然没必要真的延时,直接对数组排序即可,理解代码原理后,转换成C#
如下:
1 2 3 4 5 6 7 8 9 10 11 12 static long Func4 (long [] arr ) { Array.Sort(arr); Func<long , long , long >[] funcs = { (a, b) => a + b, (a, b) => a - b, (a, b) => a * b }; long res = 0 ; for (int i = 0 ; i < arr.Length; i++) { var f = funcs[(i + 1 ) % 3 ]; res = f(res, arr[i]); } return -res; }
第四关顺利通过,分数达到50万。
第五关 老规矩,继续查看js文件,这次得到的代码是这样:
1 window .A661E542=async function ({a:A} ) {return (await WebAssembly.instantiate(await WebAssembly.compile(await (await fetch("data:application/octet-binary;base64,AGFzbQEAAAABBwFgAn9/AX8CFwIETWF0aANtaW4AAARNYXRoA21heAAAAwIBAAcHAQNSdW4AAgpgAV4BBn8gACECIAFBAWsiBARAA0AgAiEDQQAhBkEKIQcDQCADQQpwIQUgA0EKbiEDIAUgBhABIQYgBSAHEAAhByADQQBLDQALIAIgBiAHbGohAiAEQQFrIgQNAAsLIAIL" )).arrayBuffer()),{Math :Math })).exports.Run(...A)}
WebAssembly
简称wasm
,是可在浏览器中直接运行二进制代码的解决方案,例如c/c++编译的程序。 通过上述代码可以知道,这段二进制可执行字节码编码成了base64,同时将字节码编译后得到了一个WebAssembly
的实例,这个实例包含一个Run
方法,接受参数为a
数组。 接下来,就是分成2个步骤:
将base64编码还原成二进制文件
反编译这个二进制文件,得到源代码文件
步骤1很简单,这里不再详述,得到了一个153字节的二进制文件t.wasm
,步骤2通过wabt 工具包来实现,具体可参考这篇博文WASM逆向分析 。 我将其反编译成c语言源代码,命令./wasm2c t.wasm -o out.c
,从源码里找到Run
函数的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static u32 w2c_Run (u32 w2c_p0, u32 w2c_p1) { u32 w2c_l2 = 0 , w2c_l3 = 0 , w2c_l4 = 0 , w2c_l5 = 0 , w2c_l6 = 0 , w2c_l7 = 0 ; FUNC_PROLOGUE; u32 w2c_i0, w2c_i1, w2c_i2; w2c_i0 = w2c_p0; w2c_l2 = w2c_i0; w2c_i0 = w2c_p1; w2c_i1 = 1u ; w2c_i0 -= w2c_i1; w2c_l4 = w2c_i0; if (w2c_i0) { w2c_L1: w2c_i0 = w2c_l2; w2c_l3 = w2c_i0; w2c_i0 = 0u ; w2c_l6 = w2c_i0; w2c_i0 = 10u ; w2c_l7 = w2c_i0; w2c_L2: w2c_i0 = w2c_l3; w2c_i1 = 10u ; w2c_i0 = REM_U(w2c_i0, w2c_i1); w2c_l5 = w2c_i0; w2c_i0 = w2c_l3; w2c_i1 = 10u ; w2c_i0 = DIV_U(w2c_i0, w2c_i1); w2c_l3 = w2c_i0; w2c_i0 = w2c_l5; w2c_i1 = w2c_l6; w2c_i0 = (*Z_MathZ_maxZ_iii)(w2c_i0, w2c_i1); w2c_l6 = w2c_i0; w2c_i0 = w2c_l5; w2c_i1 = w2c_l7; w2c_i0 = (*Z_MathZ_minZ_iii)(w2c_i0, w2c_i1); w2c_l7 = w2c_i0; w2c_i0 = w2c_l3; w2c_i1 = 0u ; w2c_i0 = w2c_i0 > w2c_i1; if (w2c_i0) {goto w2c_L2;} w2c_i0 = w2c_l2; w2c_i1 = w2c_l6; w2c_i2 = w2c_l7; w2c_i1 *= w2c_i2; w2c_i0 += w2c_i1; w2c_l2 = w2c_i0; w2c_i0 = w2c_l4; w2c_i1 = 1u ; w2c_i0 -= w2c_i1; w2c_l4 = w2c_i0; if (w2c_i0) {goto w2c_L1;} } w2c_i0 = w2c_l2; FUNC_EPILOGUE; return w2c_i0; }
这段代码依旧难以理解,但是通过简单修改,可以翻译成c#
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public static int w2c_Run (int w2c_p0, int w2c_p1 ) { int w2c_l2 = 0 , w2c_l3 = 0 , w2c_l4 = 0 , w2c_l5 = 0 , w2c_l6 = 0 , w2c_l7 = 0 ; int w2c_i0, w2c_i1, w2c_i2; w2c_i0 = w2c_p0; w2c_l2 = w2c_i0; w2c_i0 = w2c_p1; w2c_i1 = 1 ; w2c_i0 -= w2c_i1; w2c_l4 = w2c_i0; if (w2c_i0 > 0 ) { w2c_L1: w2c_i0 = w2c_l2; w2c_l3 = w2c_i0; w2c_i0 = 0 ; w2c_l6 = w2c_i0; w2c_i0 = 10 ; w2c_l7 = w2c_i0; w2c_L2: w2c_i0 = w2c_l3; w2c_i1 = 10 ; w2c_i0 = w2c_i0 % w2c_i1; w2c_l5 = w2c_i0; w2c_i0 = w2c_l3; w2c_i1 = 10 ; w2c_i0 = w2c_i0 / w2c_i1; w2c_l3 = w2c_i0; w2c_i0 = w2c_l5; w2c_i1 = w2c_l6; w2c_i0 = Math.Max(w2c_i0, w2c_i1); w2c_l6 = w2c_i0; w2c_i0 = w2c_l5; w2c_i1 = w2c_l7; w2c_i0 = Math.Min(w2c_i0, w2c_i1); w2c_l7 = w2c_i0; w2c_i0 = w2c_l3; w2c_i1 = 0 ; if (w2c_i0 > w2c_i1) { goto w2c_L2; } w2c_i0 = w2c_l2; w2c_i1 = w2c_l6; w2c_i2 = w2c_l7; w2c_i1 *= w2c_i2; w2c_i0 += w2c_i1; w2c_l2 = w2c_i0; w2c_i0 = w2c_l4; w2c_i1 = 1 ; w2c_i0 -= w2c_i1; w2c_l4 = w2c_i0; if (w2c_i0 != 0 ) { goto w2c_L1; } } w2c_i0 = w2c_l2; return w2c_i0; }
此时代码已经可以正常运行了,并且计算结果正确,不过遗憾的是速度还是太慢,看来还是得理解代码的含义。通过几轮调试后分析得出,这段代码的用意如下:
函数接受2个数字
取第1个数字每个十进制数位上的最大数值和最小数值
将最大数值和最小数值的乘积累加到第1个数字,并循环重复第2步
循环次数为参数中的第2个数字,循环结束后返回累加结果
用代码简单表示为:
1 2 3 4 5 6 7 8 9 10 11 12 public static long Run (long a, long b ) { while (--b > 0 ) { var arr = a.ToString().ToCharArray(); var max = long .Parse(arr.Max().ToString()); var min = long .Parse(arr.Min().ToString()); var add = max * min; a += add ; } return a; }
计算结果依然正确,说明分析没错,速度依旧慢,调试发现慢就慢在循环上,因为参数b
的数值非常大,循环次数非常多。 继续再分析一下,一旦数字中各数位出现了0
,那么最小值肯定就是0,乘积也为0,累加后数值就不会发生变化,那么接下来的后续循环都是没有必要的,于是加上这么一个判断条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static long Run (long a, long b ) { while (--b > 0 ) { var arr = a.ToString().ToCharArray(); var min = long .Parse(arr.Min().ToString()); if (min == 0 ) return a; var max = long .Parse(arr.Max().ToString()); var add = max * min; a += add ; } return a; }
就这么一个简单判断,计算速度大大提升,第五关也顺利通过,分数达到100万。
第六关
这是一段js栈式虚拟机
的代码,后面一长串数组应该就是可执行文件的二进制编码,也就是说给了虚拟机(或解释器)的源码,以及可执行文件的二进制编码,需要基于此得出程序运行逻辑。 不同于上一关采用的标准wasm
格式,可以网上寻找工具反编译,这关只能自己分析虚拟机的执行原理了。 想从页面上入手,遗憾的是,在页面上点击种树的时候,浏览器已经因为巨大的运算量卡死了,也就是说不能从页面上取得正确结果,而在网上更不可能找到相关解决方案,所以唯一的办法就是单步调试。
经过大量反复的调试,终于发现一些蛛丝马迹,其中16
代表着指令r.push("")
的地址,而一旦执行完这个函数,后续就会执行r[r.length - 1] += String.fromCharCode(e[f++])
这条指令,从内存中连续加载一串字符串,这条指令对应着68
。基于此,对关键字节进行替换:
继续分析,程序从内存中加载了某个数字后,会循环相乘,循环次数对应着参数a
的值,每次乘完后再会对一个大数取模,内存中存在的12个整数和参数a
中的12个整数正好对应上了。 根据字节码的相似度,一开始我并没有执行完,简单猜测执行逻辑可能是这样:(n[0]^a[0] * n[1]^a[1] * n[2]^a[2] ... * n[11]^a[11]) % MOD
,然后答案错了。 为了快速让程序执行到最后,用了点小聪明,一开始进入的时候,就将a
的值全部初始化为1,这样每个循环只会执行一次,同时也并不影响程序逻辑,当然最后结果肯定是错误的,不过我的目的只是想跟踪代码执行到最后。 就这样,我发现程序的逻辑原来是每2个数字为一轮,取模后再加结果累加,12个数字共执行6轮,最终是求这个结果:(n[0]^a[0] * n[1]^a[1]) % MOD + (n[2]^a[2] * n[3]^a[3]) % MOD + ...) % MOD
。 三言两语而难将这个分析过程描述清楚,唯有自己亲手调试才能深有体会,编程的路上也许并没有什么捷径,只有不放弃,一步一步去跟踪调试,才会出现灵光一闪的时刻。
最后,将这个问题翻译成代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function powMod (int_a, n, mod ) { var a = BigInt(int_a); var ret = BigInt(1 ); while (n) { if (n & 1 ) { ret = ret * a % mod; } a = a * a % mod; n >>= 1 ; } return ret; }function run (str_arr, str_nums, str_mod ) { var arr = JSON .parse(str_arr); var nums = JSON .parse(str_nums); var mod = BigInt(str_mod); var ret = BigInt(0 ); for (var i = 0 ; i < 12 ; i += 2 ) { var r0 = powMod(nums[i], arr[i], mod); var r1 = powMod(nums[i + 1 ], arr[i + 1 ], mod); var temp = (r0 * r1) % mod; ret += temp; ret = ret % mod; } return parseInt (ret).toString(); }
对于a
的n
次方取模m
(a^n%m
)运算我进行了优化,将时间复杂度从$ O(N) $ 缩减到了 $ O(log(N)) $,另外我采用了javascript
而非c#
来实现,因为javascript
的BigInt
类型可以计算大整数的乘法,而c#
即便是decimal
也无法存储该题大整数相乘后的结果。(可利用V8引擎在C#程序中执行js脚本)
这次总算能提交了,不过仅持续了1万分即宣告错误。难道这么快就进入下一关了吗? 查看js文件,发现和之前的js有了稍许变化:
函数的顺序发生了变化,比如之前的r.push("")
地址是16
,现在变成了4
;
读取的关键数字也变了,但程序逻辑并没有变。
需要获取内存中的13个数字(12个底数,1个模数)才能计算出最终结果,所以这里编写一个专门解析js内容的函数,通过查找关键字节,然后用正则表达式取出内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static KEYS DownloadJS (WebClient client, string name ) { var content = client.DownloadString($"http://159.75.70.9:8080/{name} .js" ); KEYS keys = new KEYS(); var m = Regex.Match(content, @"\[(\d{1,4},)+\d{1,4}\]" ).Value; var arr = Newtonsoft.Json.JsonConvert.DeserializeObject<int []>(m); var begin = arr[4 ].ToString(); var tochar = arr[5 ].ToString(); var partern = @"," + begin + ",(" + tochar + @",\d{1,3},)+" ; var matches = Regex.Matches(m, partern); foreach (Match match in matches) { var value = match.Value; var temp = value .Substring(begin.Length + 2 ).Split("," .ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(p => int .Parse(p)).ToArray(); StringBuilder sb = new StringBuilder(); for (int i = 1 ; i < temp.Length; i += 2 ) { sb.Append((char )temp[i]); } value = sb.ToString(); if (int .TryParse(value , out int oi)) { keys.keys.Add(oi); } else if (long .TryParse(value , out long ol)) { keys.mod = ol; } } return keys; }
主方法修改为一旦出现答案错误,就重新解析js脚本,获取新的关键数值。
1 2 3 4 5 if (res.success != 1 ) { keys = DownloadJS(client, pull.c); continue ; }
方法奏效了,从100万一直刷到200万才再次提示错误答案,第六关也顺利通过了。
关于js虚拟机的相关文章:H5应用加固防破解-js虚拟机保护方案浅谈
第七关 本关仍然是虚拟机,由于比赛时间截止到2021年3月17日,时间有限,精力有限,头发更有限,只好止步于此了。 截至写稿,仅有4人在该关种下了2棵树:
有兴趣的朋友可以自行下载研究,第七关代码 。
仅以此文,记录一次自己逆向的经验。