戴兜

戴兜的小屋

Coding the world.
github
bilibili
twitter

格子 CTF 寫作 [寶藏地圖/CSS]

這兩道題真是太有趣了!雖然標籤是逆向,但是以前端為載體,有很多 JS/CSS 奇淫巧計,我已經迫不及待地想要和大家分享了。

Treasure Map#

題目地址:http://treasure.chal.pwni.ng/

Ready your masts and set sail! Thar be treasure here if we can figure out how to find it.

Buried Treasure

Follow the map and get the booty — a pirate's work is never done.

題目#

這道題包含了 success.js、fail.js 和 0.js ~ 199.js 共兩百個一模一樣的 js 文件(引用的 SourceMap 有所不同)。

0.js

const b64 = `
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
0
1
2
3
4
5
6
7
8
9
+
/
=`;
export const go = async () => {
    const bti = b64.trim().split("\n").reduce((acc, x, i) => (acc.set(x, i), acc), new Map());
    const upc = window.buffer.shift();
    const moi = await fetch(import.meta.url).then((x) => x.text())
    const tg = await fetch(moi.slice(moi.lastIndexOf("=") + 1)).then((x) => x.json())
    const fl = tg.mappings.split(";").flatMap((v, l) =>v.split(",").filter((x) => !!x).map((input) => input.split("").map((x) => bti.get(x)).reduce((acc, i) => (i & 32 ? [...acc.slice(0, -1), [...acc.slice(-1)[0], (i & 31)]] : [...acc.slice(0, -1), [[...acc.slice(-1)[0], i].reverse().reduce((acc, i) => (acc << 5) + i, 0)]].map((x) => typeof x === "number" ? x : x[0] & 0x1 ? (x[0] >>> 1) === 0 ? -0x80000000 : -(x[0] >>> 1) : (x[0] >>> 1)).concat([[]])), [[]]).slice(0, -1)).map(([c, s, ol, oc, n]) => [l,c,s??0,ol??0,oc??0,n??0]).reduce((acc, e, i) => [...acc, [l, e[1] + (acc[i - 1]?.[1]??0), ...e.slice(2)]], [])).reduce((acc, e, i) => [...acc, [...e.slice(0, 2), ...e.slice(2).map((x, c) => x + (acc[i - 1]?.[c + 2] ?? 0))]], []).map(([l, c, s, ol, oc, n], i, ls) => [tg.sources[s],moi.split("\n").slice(l, ls[i+1] ? ls[i+1]?.[0] + 1 : undefined).map((x, ix, nl) => ix === 0 ? l === ls[i+1]?.[0] ? x.slice(c, ls[i+1]?.[1]) : x.slice(c) : ix === nl.length - 1 ? x.slice(0, ls[i+1]?.[1]) : x).join("\n").trim()]).filter(([_, x]) => x === upc).map(([x]) => x)?.[0] ?? tg.sources.slice(-2, -1)[0];
    import(`./${fl}`).then((x) => x.go());
}
//# sourceMappingURL=0.js.map

fail.js

export const go = () => {
    document.querySelector(".frame").classList.add("fail");
}

success.js

import { go as fail } from "./fail.js";
export const go = () => {
    if (window.buffer.length !== 0) {
        fail();
    } else {
        document.querySelector(".frame").classList.add("success");
    }
}

大概的校驗流程是這樣的:

  • 輸入一個有效格式的 Flag
  • 存入 window.buffer
  • 調用go()方法 (這個方法在上述 202 個腳本中均存在,網頁默認引用了 0.js,所以執行 0.js 裡的go()方法)
  • 通過某種算法找到這 202 個腳本中的另一個進行載入,執行其中的go()方法

分析#

最終的目的是讓腳本能夠載入 success.js 並執行。但是所有腳本的內容都是一樣的,可能需要從 SourceMap 下手。

{
    "version": 3,
    "sources":["0.js","1.js","2.js","3.js","4.js","5.js","6.js","7.js","8.js","9.js","10.js","11.js","12.js","13.js","14.js","15.js","16.js","17.js","18.js","19.js","20.js","21.js","22.js","23.js","24.js","25.js","26.js","27.js","28.js","29.js","30.js","31.js","32.js","33.js","34.js","35.js","36.js","37.js","38.js","39.js","40.js","41.js","42.js","43.js","44.js","45.js","46.js","47.js","48.js","49.js","50.js","51.js","52.js","53.js","54.js","55.js","56.js","57.js","58.js","59.js","60.js","61.js","62.js","63.js","64.js","65.js","66.js","67.js","68.js","69.js","70.js","71.js","72.js","73.js","74.js","75.js","76.js","77.js","78.js","79.js","80.js","81.js","82.js","83.js","84.js","85.js","86.js","87.js","88.js","89.js","90.js","91.js","92.js","93.js","94.js","95.js","96.js","97.js","98.js","99.js","100.js","101.js","102.js","103.js","104.js","105.js","106.js","107.js","108.js","109.js","110.js","111.js","112.js","113.js","114.js","115.js","116.js","117.js","118.js","119.js","120.js","121.js","122.js","123.js","124.js","125.js","126.js","127.js","128.js","129.js","130.js","131.js","132.js","133.js","134.js","135.js","136.js","137.js","138.js","139.js","140.js","141.js","142.js","143.js","144.js","145.js","146.js","147.js","148.js","149.js","150.js","151.js","152.js","153.js","154.js","155.js","156.js","157.js","158.js","159.js","160.js","161.js","162.js","163.js","164.js","165.js","166.js","167.js","168.js","169.js","170.js","171.js","172.js","173.js","174.js","175.js","176.js","177.js","178.js","179.js","180.js","181.js","182.js","183.js","184.js","185.js","186.js","187.js","188.js","189.js","190.js","191.js","192.js","193.js","194.js","195.js","196.js","197.js","198.js","199.js","fail.js","success.js"],
    "mappings":";A4DAA;A0DAA;AzEAA;AsDAA;AmGAA;AtIAA;ApBAA;A8DAA;AZAA;AxDAA;AyDAA;ALAA;A9EAA;A6HAA;AoBAA;A1BAA;A7BAA;AvCAA;AwEAA;AFAA;AuBAA;A8BAA;AHAA;AnGAA;AvBAA;A+GAA;A2BAA;A/EAA;A7CAA;ALAA;ArCAA;AqJAA;AxCAA;AoDAA;AGAA;AtEAA;AtDAA;AjEAA;AYAA;AiFAA;AhBAA;ArEAA;AkJAA;AlCAA;A9GAA;AkHAA;AnFAA;AMAA;A5CAA;AgCAA;AyJAA;AhDAA;AjFAA;AoDAA;A/FAA;A+HAA;AzIAA;A6CAA;AsBAA;A4FAA;AvFAA;A4BAA;A1DAA;A4CAA;AoGAA"
}

上面是 0.js 的 SourceMap,對當前源碼進行了詳細的映射,那麼具體映射了什麼呢?

SourceMap 中的 mappings 包含 VLQ 編碼,分號用於表示文件行,逗號表示位置,VLQ 編碼的部分是一個可變長數組,代表了映射所需的各個增量,具體可以參考文章 http://ruanyifeng.com/blog/2013/01/javascript_source_map.html

根據 SourceMap 的映射規則,腳本的 2-66 行(即 b64 變量的內容)被分別映射到不同的 66 個文件中,舉個簡單的例子,0.js 的映射關係大概是這樣:

[["60.js","A"],["118.js","B"],["45.js","C"],["99.js","D"],["198.js","E"],["64.js","F"],["44.js","G"],["106.js","H"],["94.js","I"],["38.js","J"],["95.js","K"],["90.js","L"],["12.js","M"],["137.js","N"],["157.js","O"],["131.js","P"],["102.js","Q"],["63.js","R"],["135.js","S"],["133.js","T"],["156.js","U"],["186.js","V"],["183.js","W"],["84.js","X"],["61.js","Y"],["172.js","Z"],["199.js","a"],["120.js","b"],["75.js","c"],["70.js","d"],["33.js","e"],["182.js","f"],["142.js","g"],["194.js","h"],["197.js","i"],["127.js","j"],["73.js","k"],["8.js","l"],["20.js","m"],["101.js","n"],["85.js","o"],["16.js","p"],["162.js","q"],["128.js","r"],["18.js","s"],["132.js","t"],["49.js","u"],["55.js","v"],["11.js","w"],["43.js","x"],["196.js","y"],["148.js","z"],["67.js","0"],["119.js","1"],["24.js","2"],["151.js","3"],["14.js","4"],["59.js","5"],["81.js","6"],["173.js","7"],["86.js","8"],["114.js","9"],["56.js","+"],["100.js","/"]]

而 0.js-199.js 中的代碼部分,實際上就是在對 SourceMap 進行解析,從傳入的 flag 依次取出字符,對應到特定的 js 文件。

例如對於一個 B 開頭的 flag,就會去請求 118.js,解析 118.js 的 SourceMap,並處理 flag 的第二個字符,以此類推。

我們可以嘗試去尋找哪個文件包含對 success.js 的映射,這樣就可以確定 Flag 的最後一個字符和其對應文件,一步一步反推就能得到最終的 Flag。

相關腳本#

下載文件#

首先當然是要把所有 SourceMap 給下載下來,這裡提供一個 NodeJS 腳本

async function download() {
  const fs = require("fs");

  for (let i = 0; i < 200; i++) {
    const url = `http://treasure.chal.pwni.ng/${i}.js.map`;
    console.log(url);
    const data = await fetch(url).then((res) => res.text());
    fs.writeFileSync(`./originmaps/${i}.js.map`, data);
  }
}

download();

腳本會將所有的 SourceMap 下載到 originmaps 文件夾中。(記得提前創建文件夾)

解析 SourceMap#

稍微修改一下題目給的 js,解析 SourceMap,並將映射表保存到文件中。

// char2js.js
const fs = require("fs").promises;
const { VLQDecode, getSource, JS_SOURCE } = require("./utils.js");

(async function () {
  // 遍歷 originmaps 所有文件
  const maps = await fs.readdir("./originmaps");
  for (const map of maps) {
    // console.log(map);
    // 讀取文件內容
    const content = JSON.parse(
      await fs.readFile(`./originmaps/${map}`, "utf-8")
    );
    let _map = getCharFileMap(content);
    // 寫入到 char2js 文件夾
    await fs.writeFile(`./char2js/${map}`, JSON.stringify(_map));
  }
})();

const getCharFileMap = (content) => {
  const source = JS_SOURCE;

  const lines = content.mappings.split(";");

  const fl = lines
    .flatMap((item, index) => {
      // 位置切分
      const pos = item.split(",").filter((x) => !!x);

      // 解碼
      const decodedPos = pos.map((input) => VLQDecode(input));

      return decodedPos
        .map(([c, s, ol, oc, n]) => [
          index,
          c,
          s ?? 0,
          ol ?? 0,
          oc ?? 0,
          n ?? 0,
        ])
        .reduce(
          (acc, e, i) => [
            ...acc,
            [index, e[1] + (acc[i - 1]?.[1] ?? 0), ...e.slice(2)],
          ],
          []
        );
    })
    .reduce(
      (acc, e, i) => [
        ...acc,
        [
          ...e.slice(0, 2),
          ...e.slice(2).map((x, c) => x + (acc[i - 1]?.[c + 2] ?? 0)),
        ],
      ],
      []
    )
    .map(([l, c, s, ol, oc, n], i, ls) => [
      getSource(s),
      source
        .split("\n")
        .slice(l, ls[i + 1] ? ls[i + 1]?.[0] + 1 : undefined)
        .map((x, ix, nl) =>
          ix === 0
            ? l === ls[i + 1]?.[0]
              ? x.slice(c, ls[i + 1]?.[1])
              : x.slice(c)
            : ix === nl.length - 1
            ? x.slice(0, ls[i + 1]?.[1])
            : x
        )
        .join("\n")
        .trim(),
    ]);

  return fl;
};

該文件使用了 utils.js,可以在這裡下載:https://ipfs.4everland.xyz/ipfs/QmSDubw4sHg25kSjzzQu9aotV52bbbRp9nZbxog5KcbfDX

尋找正確的加載路徑#

const fs = require("fs").promises;
const jsMaps = {};
function calcPath(curFlagPath, currentJsCursor) {
  if (currentJsCursor == "0.js") {
    console.log(curFlagPath.reverse().join(""));
    return;
  }
  for (let map of Object.keys(jsMaps)) {
    let flag = false;
    for (const [file, char] of jsMaps[map]) {
      if (file == currentJsCursor) {
        if(currentJsCursor === map) flag = true;
        flag = true
        let _curFlagPath = [...curFlagPath];
        _curFlagPath.push(char);
        currentJsCursor = map.replace(".map", "");
        calcPath(_curFlagPath, currentJsCursor);
      }
    }
    if (flag) break;
  }
}

(async function () {
  // 遍歷 char2js 所有文件
  const maps = await fs.readdir("./char2js");
  for (const map of maps) {
    // 讀取文件內容
    const content = JSON.parse(await fs.readFile(`./char2js/${map}`, "utf-8"));
    jsMaps[map] = content;
  }

  calcPath([], "success.js");
})();

錯了🤯🤯#

最終我們能得到一個 23 位的 Flag: Nd+a+map/How+about+200!,但是題目要求 25 位。說明中間可能會存在多條路徑,下面的腳本是柏喵改進的,tql

const fs = require("fs").promises;
const jsMaps = {};
let final = [];
(async function () {
  // 遍歷 char2js 所有文件
  const maps = await fs.readdir("./char2js");
  for (const map of maps) {
    // 讀取文件內容
    const content = JSON.parse(await fs.readFile(`./char2js/${map}`, "utf-8"));
    jsMaps[map] = content;
  }

  let currentJsCursor = [{"cur":"success.js","path":[]}];
  let nextJsCursor = [];
  for (i = 0; i < 25; i++) {
    for (let map of Object.keys(jsMaps)) {
      for (const [file, char] of jsMaps[map]) {
        currentJsCursor.filter((item) => item.cur === file)
          .forEach(
            (item) => nextJsCursor.push({cur:map.replace('.map',''),path:[...item.path,char]}
          ));
      }
    }
    currentJsCursor = nextJsCursor.reduce((acc, cur) => {
      if (acc.findIndex((item) => item.cur === cur.cur) === -1) {
        acc.push(cur);
      }
      return acc;
    }, []);
    nextJsCursor = [];
  }
  currentJsCursor.filter((item) => item.cur === "0.js").forEach((item) => console.log(item.path.reverse().join(""))); 
})();

之前錯誤 Flag 的路徑是 0.js->137.js->160.js->192.js->... (Nd+a...)

正確 Flag 的路徑是 0.js->137.js->23.js->137.js->160.js->192.js->... (Need+a...)

得到正確的 Flag PCTF{Need+a+map/How+about+200!}

CSS#

題目地址:https://plaidctf.com/files/css.74486b61b22e49b3d8c5afebee1269e37b50071afbf1608b8b4563bf8d09ef92.html

I found this locked chest at the bottom o' the ocean, but the lock seems downright... criminal. Think you can open it? We recommend chrome at 100% zoom. Other browsers may be broken.

image

這道題是一個完全由 CSS 構成的密碼鎖。前天我就發了推吐槽這道題的 CSS 樣式。(能整出這種活的人真是太牛了)

分析#

Flag 包含小寫字母a-z以及下劃線_,以三個字符為一組,分成了 14 組共 42 位。

字符的選擇是通過 details 標籤來實現的,details 的子元素擁有不同的高度,使用 css 的calc函數來獲取高度並運算,得到字符元素的偏移量,達到顯示字符的目的。

每 3 個字符會控制 4 個 SVG 蒙版。拿了 8 個來舉例子:

image

灰色部分是每個 SVG 透明的位置,倘若每個 SVG 的位置正確,最終應該是這個效果:

image

由於 details 的伸縮與展開會影響到父容器高度,SVG 蒙版的父元素也在這個容器中,高度也會發生改變,而 SVG 的 top 屬性通過父元素高度計算得來。

慶幸的是,SVG 以 dataurl 的方式作為背景圖片,但是這個元素的高度是固定的,所以不需要考慮背景的各種填充方式 (cover/contain等),換句話說,想要解出正確的 Flag,SVG 最終的 top 值一定只和其透明位置的高度有關。

解題#

我沒有去看各個 detail 標籤和 SVG 容器的高度是如何變化的,這實在太多了(或許可以嘗試使用 Typed OM 輔助分析?)

不過每組 SVG 只由 3 個字符控制,也就是最終只需要嘗試 27^3 種情況,決定直接通過暴力遍歷的方式來解決。

這裡就有一個問題:如何知道 SVG 已經到了正確的位置?

同學想用無頭瀏覽器進行圖像匹配,這顯然是行不通的,效率太低,且需要對每組 SVG 進行隔離才能正確識別。

我想到的是,如果能夠獲取到每個 SVG 的top值,那麼就可以通過計算得到其透明區域的高度,然後與預期的高度進行比較,如果相等,那麼就說明這個 SVG 已經到了正確的位置。

這其實很好辦,分成下面幾步:

  1. 如何拿到當前 SVG 的 top 值?

SVG 的 top 樣式是通過 calc 計算得來的,可能一開始會覺得很難獲取,但實際上,瀏覽器提供了接口 window.getComputedStyle,通過這個接口,能夠得到元素計算之後的樣式數值。

const getCurrentPosByIndex = (index) => {
    return window.getComputedStyle(
        document
            .querySelectorAll('[style*="url(\'data:image/svg+xml;base64,"]')[index])
            .top.slice(1,-2) - 0
}
  1. 每個 SVG 中透明區域的位置在哪?
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="540"><path fill="#fff" d="M0 0H200V540H0ZM2 22V38H198V22Z"/></svg>

上面這個 SVG 的透明區域是M2 22V38H198V22Z,不難看出,這個區域的上邊界是 22,不過由左邊界 2 可以看出,開發者可能希望透明區域具有 2px 的邊距,大膽猜測,透明區域的上邊界應該是 20

所有 SVG 蒙版都被處理成 dataurl ,以內聯背景樣式的形式嵌入到頁面中,這也為編寫腳本提供了極大的便利。只需要按次序取出蒙版,然後使用正則匹配出透明區域的上邊界即可。

[
  ...document.querySelectorAll('[style*="url(\'data:image/svg+xml;base64,"]'),
].reduce((acc, el) => {
  const svg = el.style.backgroundImage
    .replace(/^url\("data:image\/svg\+xml;base64,/, "")
    .replace(/"\)$/, "");
  return acc.push(/ZM\d+\s(\d+)/.exec(atob(svg))[1] - 2), acc;
}, []);

得到的結果是:

[60, 40, 440, 120, 20, 80, 240, 140, 140, 140, 100, 120, 80, 300, 200, 160, 80, 80, 180, 220, 440, 40, 80, 220, 260, 140, 120, 120, 0, 200, 120, 300, 0, 140, 240, 120, 20, 120, 300, 120, 280, 20, 320, 60, 80, 120, 180, 0, 300, 20, 120, 80, 20, 120, 40, 20]
  1. 每個 SVG 最終正確的 top 值應該是多少?

需要注意的是:原 HTML 中綠色 currect! 字樣在距離頂部 60px 處,這個值也需要考慮進去。

[
  ...document.querySelectorAll('[style*="url(\'data:image/svg+xml;base64,"]'),
].reduce((acc, el) => {
  const svg = el.style.backgroundImage
    .replace(/^url\("data:image\/svg\+xml;base64,/, "")
    .replace(/"\)$/, "");
  return acc.push(60 - (/ZM\d+\s(\d+)/.exec(atob(svg))[1] - 2)), acc;
}, []);

可以得到所有 SVG 最終所需的正確 top 值:

[0, 20, -380, -60, 40, -20, -180, -80, -80, -80, -40, -60, -20, -240, -140, -100, -20, -20, -120, -160, -380, 20, -20, -160, -200, -80, -60, -60, 60, -140, -60, -240, 60, -80, -180, -60, 40, -60, -240, -60, -220, 40, -260, 0, -20, -60, -120, 60, -240, 40, -60, -20, 40, -60, 20, 40]

我們嘗試將每個 SVG 的 top 值設置為正確的值,顯示出了綠色的 currect! 字樣,說明這些位置是正確的!

後來想想其實getBoundingClientRect().y也能拿到,繞了個大弯

  1. 如何改變某一位字符?

這是最關鍵的,因為我們需要通過改變某一位字符來改變 SVG 的 top 值,從而達到移動 SVG 的目的。

然而這個網頁完全由 CSS 實現,想直接修改字符當然是行不通的。

那麼模擬點擊兩個紅色上下箭頭能行么?也不行。網頁在每個箭頭處堆疊了 26 個 detail 標籤,通過給 detail 設置偏移來實現紅色箭頭位置在每次點擊時都能操作到不同的 detail。

所以,只能嘗試直接用 js 去操作 detail 標籤屬性。當 detail 打開時,其元素本身會具有 open 屬性。我們只需要操縱這個屬性,就能實現打開和關閉 detail 標籤的效果。

尋找規律能發現,倒數第一個 detail 打開時,字符為 a;倒數后二個 detail 打開時,字符為 b,以此類推。

下面是一個簡單的腳本,用於改變某一位字符:

setCharOfSlot(0,'b') 即將第0位設置為b

let containers = document.querySelectorAll('[style*="transform:rotate(0deg)"]');
let charMap = "abcdefghijklmnopqrstuvwxyz_";

const setBits = (bits, char) => {
  let index = charMap.indexOf(char);
  for (let i = 0; i < bits.length; i++) {
    if (i === index) {
      bits[i].setAttribute("open", "");
    } else {
      bits[i].removeAttribute("open");
    }
  }
};

const setCharOfSlot = (slot, char) => {
  const container = containers[Math.floor(slot / 3)];
  const bits = [...container.children].slice(
    (slot % 3) * 26,
    ((slot % 3) + 1) * 26
  );

  setBits(bits, char);
};

接下來,萬事俱備,可以開始嘗試遍歷了!使用 requestAnimationFrame 來控制遍歷速度避免卡頓,同時支持了進度保存,跑出結果大概需要 3 分鐘。

let correctPosition = [
  0, 20, -380, -60, 40, -20, -180, -80, -80, -80, -40, -60, -20, -240, -140,
  -100, -20, -20, -120, -160, -380, 20, -20, -160, -200, -80, -60, -60, 60,
  -140, -60, -240, 60, -80, -180, -60, 40, -60, -240, -60, -220, 40, -260, 0,
  -20, -60, -120, 60, -240, 40, -60, -20, 40, -60, 20, 40,
];

const allMasks = document.querySelectorAll(
  '[style*="url(\'data:image/svg+xml;base64,"]'
);

const getCurrentPosByIndex = (index) => {
  return allMasks[index].offsetTop;
};

let containers = document.querySelectorAll('[style*="transform:rotate(0deg)"]');
let charMap = "abcdefghijklmnopqrstuvwxyz_";

const setCharOfSlot = (slot, char) => {
  const container = containers[~~(slot / 3)];

  let start  = (slot % 3) * 26
  let end = ((slot % 3) + 1) * 26
    let index = charMap.indexOf(char);
    for (let i = start; i < end; i++) {
      if (i - start < index) {
        container.children[i].setAttribute("open", "");
      } else {
        container.children[i].removeAttribute("open");
      }
    }
};

// 使用 charMap 生成 3 位的所有可能
const allChars = [];let reversedCharMap = "_zyxwvutsrqponmlkjihgfedcba";
for (let i = 0; i < reversedCharMap.length; i++) {
  for (let j = 0; j < reversedCharMap.length; j++) {
    for (let k = 0; k < reversedCharMap.length; k++) {
      allChars.push(reversedCharMap[i] + reversedCharMap[j] + reversedCharMap[k]);
    }
  }
}

const map2string = (map) => {
  // map轉json
  let json = JSON.stringify([...map]);
  return json;
};

const string2map = (str) => {
  // json轉map
  let map = new Map(JSON.parse(str));
  return map;
};

let currentCharCase = ~~localStorage.getItem("currentCharCase") || 0;
let _tmp = localStorage.getItem("solvedGroup");
let solvedGroup = _tmp ? string2map(_tmp) : new Map();
let curSolvedGroup = new Set();
let cacheCount = 0;
let first = true;

const bruteForce = () => {
  if (
    ([...solvedGroup.keys()].length === 14 ||
      currentCharCase === allChars.length) &&
    !first
  ) {
    console.log("done");
    return;
  }
  first = false;
  let chars = allChars[currentCharCase];
  if (cacheCount++ === 100) {
    localStorage.setItem("currentCharCase", currentCharCase);
    cacheCount = 0;
  }

  for (let group = 0; group < 14; group++) {
    if (curSolvedGroup.has(group)) continue;
    if (solvedGroup.has(group)) {
      let char = solvedGroup.get(group);
      setCharOfSlot(group * 3, char[0]);
      setCharOfSlot(group * 3 + 1, char[1]);
      setCharOfSlot(group * 3 + 2, char[2]);
      curSolvedGroup.add(group);
      continue;
    }
    let solvedMask = 0;
    for (let j = 0; j < 4; j++) {
      let currentPos = getCurrentPosByIndex(group * 4 + j);
      let correctPos = correctPosition[group * 4 + j];

      if (Math.abs(correctPos - currentPos) < 4) {
        solvedMask += 1;
      }
    }
    if (solvedMask === 4) {
      console.log(
        `Group ${group} is solved, chars are "${allChars[currentCharCase - 1]}"`
      );
      solvedGroup.set(group, allChars[currentCharCase - 1]);
      curSolvedGroup.add(group);
      localStorage.setItem("solvedGroup", map2string(solvedGroup));
      continue;
    } else {
      setCharOfSlot(group * 3, chars[0]);
      setCharOfSlot(group * 3 + 1, chars[1]);
      setCharOfSlot(group * 3 + 2, chars[2]);
    }
  }
  currentCharCase++;
  requestAnimationFrame(bruteForce);
};

requestAnimationFrame(bruteForce);
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。