开始

2021年3月12日,植树节,腾讯发起了极客技术挑战赛,规则很简单,在页面上只有一个种树按钮,点击一下,就种下了一棵树。树种得越多越好,排行榜会根据树的数量排名。

本期的比赛秉承了极客技术挑战赛的一贯传统,它非常简单,几乎不需要写代码,你只需要简单地点击最下方种树按钮就可以了,最终谁种的树最多,谁就是冠军!
比赛截止后,种下的树数量越多排名越靠前,如遇数量相同,则按照到达该数量的时间排名。

页面在此:码上种树

作为程序员,一定会本能的按下F12,打开开发者工具。一是查看点击按钮执行的代码,二是查看提交给服务器的数据包。
通过分析找到关键的js代码,

这段代码通过分析含义如下:

  1. 通过pull请求获得数据字段a和js文件名c字段
  2. 动态加载这个js文件,然后将pull请求返回的结果传递给js执行
  3. 将执行完的结果以及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"); //这个id和登录的账号相关
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];
}
//temp % arr[0x2] == arr[0x1] && i < res && (res = i);
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个步骤:

  1. 将base64编码还原成二进制文件
  2. 反编译这个二进制文件,得到源代码文件

步骤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_l2=a
w2c_i0 = w2c_p1;
w2c_i1 = 1u; //w2c_i1=1
w2c_i0 -= w2c_i1; //b-=1
w2c_l4 = w2c_i0; //w2c_l4=b
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_l2=a
w2c_i0 = w2c_p1;
w2c_i1 = 1; //w2c_i1=1
w2c_i0 -= w2c_i1; //b-=1
w2c_l4 = w2c_i0; //w2c_l4=b
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;
}

此时代码已经可以正常运行了,并且计算结果正确,不过遗憾的是速度还是太慢,看来还是得理解代码的含义。通过几轮调试后分析得出,这段代码的用意如下:

  1. 函数接受2个数字
  2. 取第1个数字每个十进制数位上的最大数值和最小数值
  3. 将最大数值和最小数值的乘积累加到第1个数字,并循环重复第2步
  4. 循环次数为参数中的第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) { //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();
}

对于an次方取模ma^n%m)运算我进行了优化,将时间复杂度从$ O(N) $ 缩减到了 $ O(log(N)) $,另外我采用了javascript而非c#来实现,因为javascriptBigInt类型可以计算大整数的乘法,而c#即便是decimal也无法存储该题大整数相乘后的结果。(可利用V8引擎在C#程序中执行js脚本)

这次总算能提交了,不过仅持续了1万分即宣告错误。难道这么快就进入下一关了吗?
查看js文件,发现和之前的js有了稍许变化:

  1. 函数的顺序发生了变化,比如之前的r.push("")地址是16,现在变成了4
  2. 读取的关键数字也变了,但程序逻辑并没有变。

需要获取内存中的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棵树:

有兴趣的朋友可以自行下载研究,第七关代码


仅以此文,记录一次自己逆向的经验。