2021 魔盒挑战第一期-网页迷宫篇 解题流程与说明

Level 00

略。

Level 01 | HelloMaze.html

用鼠标选中色块中的文字即可,得到答案eA5JlOGrq0AFEPy6IJT

Level 010 | eA5JlOGrq0AFEPy6IJT.html

根据提示打开审查元素,在元素/Inspector一栏用元素选择器选择带边框的div,展开标签看到hiddentext:UDZvYvFch,即为答案。

Level 011 | UDZvYvFch.html

根据提示打开审查元素的控制台,此时点击按钮可以看到控制台输出EiuOOcxCOKzEyXLL,即为答案。

Level 0100 | EiuOOcxCOKzEyXLL.html

Method 1

根据提示打开审查元素的控制台,输入指令answerconsole.log(answer)即可看到答案:rMhsaAmoDVeRXBcdoN

Method 2

在审查元素的元素一栏中查找script标签,看到

1
<script>var answer="rMhsaAmoDVeRXBcdoN" </script>

一行,说明answer的值为rMhsaAmoDVeRXBcdoN

Level 0101 | rMhsaAmoDVeRXBcdoN.html

按照题目要求写出程序并运行即可得到答案。两个样例程序如下:

python

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
import math


def proc(st):
nums = list(map(ord, st))
avg = sum(nums)/len(nums)
var = sum(map(lambda x: x**2, nums))/len(nums)-avg**2
avg, var = math.ceil(avg), math.ceil(var)
return avg, var


def proc2(num):
while not(48 <= num <= 57 or 65 <= num <= 90 or 97 <= num <= 122):
num = (num+7) % 129
return chr(num)


lst = [
"HelloMaze",
"eA5JlOGrq0AFEPy6IJT",
"UDZvYvFch",
"EiuOOcxCOKzEyXLL",
"rMhsaAmoDVeRXBcdoN"
]

print(''.join([proc2(j) for i in lst for j in proc(i)]))

JavaScript

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
function processNum(n){
n=Math.ceil(n)
while(n<48|57<n&n<65|90<n&n<97|n>122){
n=(n+7)%129;
}
return String.fromCharCode(n);
}

function processStr(s){
let sum=0,squaredsum=0;
for(let i=0;i<s.length;i++){
let n=s.charCodeAt(i);
sum+=n;
squaredsum+=n*n;
}
let avg=sum/s.length;
let variance=squaredsum/s.length-avg*avg;
return processNum(avg)+processNum(variance);
}

let answer="";
let lst=[
"HelloMaze",
"eA5JlOGrq0AFEPy6IJT",
"UDZvYvFch",
"EiuOOcxCOKzEyXLL",
"rMhsaAmoDVeRXBcdoN"
];
lst.forEach((value)=>{
answer+=processStr(value);
})
console.log(answer);

Level 0110 | dkPBe3b2d3.html

根据提示打开审查元素,找到check的源代码(注释是后加的):

1
2
3
4
5
6
7
8
9
10
function check(m,n,o,p){
let ns = [m,n,o,p].sort((a,b)=>b-a); // 将[m,n,o,p]逆序排序后存入ns内,说明参数的顺序不会对影响结果
let nums = [];
for(let i=2; i<5; i++){ // i从2到4迭代
ns.forEach((value)=>{ // 对列表ns迭代
nums.push(value*i); // 将value*i加到nums末尾
})
}
return String.fromCharCode(...nums); // 将nums转换为字符串并返回
}

分析后可知check函数负责将四个数字,m、n、o、p,倒序排序后都乘以2、3、4,将结果依次加入数组nums内。随后将nums的各项按照ascii码转换为字符串。

这里再结合提示“联系上一题”,为了确保整数对应的字符是合规的答案,整数n必须满足如下约束:

2n,3n,4nZ{[48,57][65,90][97,122]}2n,3n,4n\in \mathbb Z\cap\{[48,57]\cup[65,90]\cup[97,122]\}

24.25=max{24,653,974}nmin{572,903,1224}=28.524.25=\max\{24,\frac{65}{3},\frac{97}{4}\}\leq n \leq \min\{\frac{57}{2},\frac{90}{3},\frac{122}{4}\}=28.5

又因为题目中规定了check的输入都是整数,所以满足条件的只有25,26,27,28四个数,共有35种情况。可以写程序穷举也可以手工穷举,不过在穷举之前应该能想到把四个数都填进去试试看,正好就能得到答案8642TQNKplhd

穷举程序(python):

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
import requests as r   # python中著名的网络请求库,需通过pip安装
from time import sleep


def check(*args): # 页面中的check函数的复刻
return "".join([chr(i*j) for j in range(2, 5) for i in sorted(args, reverse=True)])


nums = [25, 26, 27, 28]

conditions = (
(nums[i], nums[j], nums[k], nums[l])
for i in range(0, 4)
for j in range(i, 4)
for k in range(j, 4)
for l in range(k, 4)
) # 列举所有情况

for cond in conditions:
answer = check(*cond)
url = f"http://10.10.65.208:8080/maze/{answer}.html"
response = r.get(url, timeout=1)
if response.status_code == 200: # 页面存在时状态码为200
print(f'{cond} => {url} exist.')
print("\n======================")
print(f"The answer: {answer}\nLink to next level: {url}")
print("======================\n")
elif response.status_code == 404: # 页面不存在时状态码为404
print(f'{cond} => {url} not exist.')
else: # 如果状态码不是上述二者说明服务器出问题或者访问过于频繁,抛出异常
raise BaseException(
f"unknown status code: {response.status_code} while accessing {url}.")
sleep(0.5) # 控制访问间隔,防止对服务器造成压力

Level 0111 | 8642TQNKplhd.html

题目中的色块共有6行8列,且只有8种颜色,结合提示猜测通关的信息可能被编码成颜色,因此先按顺序提取出色块中的所有颜色,发现颜色的rgb值中各分量只有00ff两种值,若是将00记为0,将ff记为1,即可得到一个仅由0与1组成的二进制串:

1
011100100101001000110000010100100100100101010110010110100011000101010100001110010011010101100100001101000111010001111000011100000110100101100101

因为1 byte=8 bits,所以将上述二进制串每八位分为一组,再将每组转化为十进制,得到一个长度为18的数组:

1
[114, 82, 48, 82, 73, 86, 90, 49, 84, 57, 53, 100, 52, 116, 120, 112, 105, 101]

注意到数组内每个数都在128以内,猜测这可能是ascii码值,于是将数组转化为字符串,得到rR0RIVZ1T95d4txpie,即本题的答案。

完整代码(javascript)如下(在审查元素的控制台中运行):

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 () {
// 用正则表达式按顺序获取table中的所有颜色
const text = document.getElementsByClassName("colormatrix")[0].innerHTML;
let colors = [...text.matchAll(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/g)];

// 转换为比特
let bits = [];
colors.forEach((value) => {
bits.push(value[1] === "00" ? 0 : 1, value[2] === "00" ? 0 : 1, value[3] === "00" ? 0 : 1);
})
console.log(bits);

// 转换为字符串
let nums = [];
let n = 0;
while (n < bits.length) {
let num = 0;
for (let i = 0; i < 8; i++, n++) {
num = num * 2 + bits[n]
}
nums.push(num);
}
const answer = String.fromCharCode(...nums);

// 输出结果
console.log(answer);
})();

Level 01000 | rR0RIVZ1T95d4txpie.html

提示中已经给出这一题是上一题的强化版,因此我们仿照上一题,先提取出表中的所有颜色代码。

从页面获得二进制串

但需要注意的是,上一题所用的使用正则表达式#[0-9a-fA-F]{6}提取颜色代码的方式在这里并不适用,因为表中存在有以下三种形式的单元格:

1
2
3
<td style="background:#102024"></td>
<td style="background:#040"></td>
<td></td>

其中第一种我们在上一题遇到过了,第二种里#040#004400的简写形式,而第三种,结合.colormatrix上方的样式表:

1
2
3
4
5
.colormatrix td{
width: .5em;
height: .5em;
background-color: black;
}

可知是黑色,即#000000

因此我们在提取颜色的时候需要兼顾此三种,提取颜色后我们要将其转换为二进制串,完整代码如下(在审查元素的控制台中运行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getbits(){
const blocks=document.getElementsByTagName("td");
let bits=[];
const black=[0,0,0];
const digits=[7,6,5,4,3,2,1,0];
for(let i=0;i<blocks.length;i++){
let color;
if(blocks[i].style.background===""){
color=black;
}else{
let c=blocks[i].style.backgroundColor.match(/\d+/g);
color=c.map((value)=>{return Number.parseInt(value)})
}
color.forEach((value)=>{
bits.push(...digits.map((i)=>{
return (value>>i)&1;
}))
})
}
return bits;
}

const bits=getbits();

从bmp文件获得二进制串

在colormatrix的下方有一条注释:

1
<!--  http://localhost:4580/PtdqW.bmp  -->

这个url(实际线上版本与此不同)指向一张bmp图片,其内容与上方表格相同,因此我们也可以从这里提取颜色。

下载图片后我们用python的Pillow库与numpy库(需要用pip安装)来处理图片:

1
2
3
4
5
6
7
8
from PIL import Image
import numpy as np

with Image.open("./PtdqW.bmp", mode='r') as im:
ar = np.array(im)

ar = ar.reshape((20*89*3, 1))
bits = np.hstack([(ar >> i) & 1 for i in range(7, -1, -1)])

将二进制串转化为图片

经过上述两个操作的任一个我们都能得到一个含有42720个比特的数组。再结合第二条提示267x160,发现267*160=42720,因此我们只需将数组填入宽160高267的矩阵中即可。

python的完整程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from matplotlib import pyplot as plt
from PIL import Image
import numpy as np

with Image.open("./PtdqW.bmp", mode='r') as im:
ar = np.array(im)

ar = ar.reshape((20*89*3, 1))
bits = np.hstack([(ar >> i) & 1 for i in range(7, -1, -1)])
bits = bits.reshape((267, 160))

plt.imshow(bits)
plt.savefig("./output.jpg")

输出的图片如下:

output

在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
function showimg(bits){
const el=document.createElement("table");
el.className="colormatrix2";
let k=0;
while(k<bits.length){
let tr=document.createElement("tr")
let t='';
for(let i=0;i<160;i++,k++){
t+=bits[k]?"<td></td>":`<td class="white"></td>`;
}
tr.innerHTML=t;
el.appendChild(tr);
}
const style=document.createElement("style")
style.innerHTML=`
.colormatrix2 td{
width: .1em;
height: .1em;
background-color: black;
}
.colormatrix2 td.white{
background-color: white;
}
`
const parent=document.getElementsByClassName("flexbox")[0];
parent.appendChild(style);
parent.appendChild(el);
}

showimg(bits);

可以在页面中看到如下图像:

showimg

亦可将上面的python程序的后两行改为:

1
2
3
with open("a.txt","w") as f:
for r in bits:
print(*("*" if i else ' ' for i in r),sep="",file=f)

运行程序后,打开a.txt,在合适的字体、字号与行距下可以看到由字符组成的图片。

通过图片我们可以看出这张图中的是埃菲尔铁塔,其英文名是The Eiffel TowerEiffel Tower,又因为题目要求“答案为由驼峰命名法首字母小写)结合而成的三个单词”,所以答案是theEiffelTower

注:图片来源:https://commons.wikimedia.org/wiki/File:Eiffel_Tower_1945.jpg 原图属于公有领域,在引用时做了亿点点处理以压缩体积。

Level 01001 | theEiffelTower.html

Base64解码

首先观察题目给的数据,根据提示得知这是使用base64编码的二进制数据,因此首先使用base64对其解码。

1
2
3
4
from base64 import b64decode

data = b64decode("ASJcDiBgWReEMk5KyjCGSOi0OQNT")
print(data)

得到一串二进制数据:

1
b'\x01"\\\x0e `Y\x17\x842NJ\xca0\x86H\xe8\xb49\x03S'

校验

根据提示,接下来要用汉明码进行校验,因此先将其转化为仅含0与1的数组:

1
2
3
bits = [i for n in data for i in map(
lambda power:(n >> power) & 1, range(7, -1, -1))]
print(*bits,sep="")

输出

1
000000010010001001011100000011100010000001100000010110010001011110000100001100100100111001001010110010100011000010000110010010001110100010110100001110010000001101010011

然后校验:

1
2
3
4
5
6
7
8
9
10
11
from functools import reduce

pow = 1
while (1 << pow)-1 < len(bits):
pow += 1

parities = [0]*pow
for digit in range(pow):
parities[-digit-1] = reduce(lambda x, y: x ^ y,
(v for i, v in enumerate(bits) if (i+1 >> digit) & 1), 0)
print(parities)

输出:[0, 0, 0, 1, 1, 0, 0, 0](00011000)2=24(00011000)_2=24,说明从1开始数的第24位发生了翻转(当然也可能有多位发生了翻转,不过那样题目就做不下去了,故不在考虑范围内),因此对数组的第23位进行修正:

1
bits[23]=0 if bits[23] else 1

然后剔除数据中的校验位:

1
2
checkedbits = [v for i, v in enumerate(bits) if i & (i+1)]
print(*checkedbits,sep="")

得到:

1
0000001000101011101000011100100000011000000101100100010111000010000110010010011100100101011001010001100001000011001001001110100010110100001110010000001101010011

解码

根据提示的第三条Zeckendorf定理我们可以在搜索引擎中找到其在编码领域的应用,即斐波那契编码。斐波那契编码仅有0与1组成,其中每一位的位值都对应一个斐波那契数,将斐波那契编码中为1的位对应的位值求和即可得到编码表示的十进制数,例如(1010101)fib=21+8+3+1=33(1010101)_{fib}=21+8+3+1=33。Zeckendorf定理能确保在合适的条件下,一个数在斐波那契编码中不会出现两个连续的1。若是在每个数的末尾补充一个1,这个1与下一个数首位的1连在一起形成“11”,我们可以据此区分编码串中相邻的两个数:

  • 若出现连续的两个1,则上一个数以0结尾,以第一个1作为分隔符;
  • 若出现连续的三个1,则上一个数以1结尾,以第二个1作为分隔符。

上面的二进制串可以依此法分为:

1
000000100010101 10100001 1001000000 100000010 1001000101 1000010000 1001001001 1001001010 100101000 1000010000 1001001001 10100010 10100001 1001000000 10101001

然后将斐波那契编码转换为十进制,得到:

1
67 48 110 57 114 97 116 117 73 97 116 49 48 110 53

最后按ascii码表转换为字符,得到答案:C0n9ratuIat10n5

解码的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 斐波那契数列
fib = [1, 1]
while fib[-1] <= 1024:
fib.append(fib[-2]+fib[-1])

checkedbits.pop() # 忽略最后一个数末尾添的1
nums = []
n, lastbit, p = 0, 0, 1
for i in reversed(checkedbits):
if i and lastbit:
nums.append(n)
n, lastbit, p = 0, 0, 1
else:
if i:
n += fib[p]
lastbit = i
p += 1
if n:
nums.append(n)

answer = ''.join(map(chr, reversed(nums)))
print(answer)

Bonus的获取方法

Bonus 0: iWNDvqJY

在Level 0打开审查元素,发现在文字的第二、三段之间隐藏了一个元素:

1
<p style="display: none;">Bonus0: iWNDvqJY</p>

Bonus 1: 8864TTQNpplh

在Level 6中穷举时可以得到两个存在的页面,其中8642TQNKplhd指向Level 7,另一个8864TTQNpplh即为Bonus 1。

Bonus 2: laTourEiffel

在Level 8得到答案前的最后一步,法文单词怎么能不算单词呢(逃)。