この 2 つの問題は本当に面白いです!タグは逆向きですが、フロントエンドを媒体として、多くの JS/CSS の奇妙なトリックがあります。私はもう皆さんと共有したくてたまりません。
宝の地図#
問題のアドレス:http://treasure.chal.pwni.ng/
マストを準備して出航せよ!ここには宝がある、見つける方法がわかれば。
埋蔵された宝
地図に従って宝を手に入れろ — 海賊の仕事は決して終わらない。
問題#
この問題には success.js、fail.js、そして 0.js から 199.js までの合計 200 個の同一の 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 の 2 文字目を処理する、という具合です。
私たちは、どのファイルが 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) {
// ファイル内容を読み込む
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
私は海の底でこのロックされた宝箱を見つけましたが、ロックはまるで... 犯罪のようです。開けることができると思いますか?私たちは 100%のズームで Chrome を推奨します。他のブラウザは壊れているかもしれません。
この問題は完全に CSS で構成されたパスワードロックです。前日、私はこの問題の CSS スタイルについてツイートしました。(こんなことができる人は本当にすごいです)
分析#
Flag には小文字のアルファベットa
-z
とアンダースコア_
が含まれ、3 文字ごとにグループ化され、合計 14 グループ 42 文字になります。
文字の選択は details タグを通じて実現され、details の子要素は異なる高さを持ち、CSS のcalc
関数を使用して高さを取得し計算し、文字要素のオフセットを得て、表示文字を実現します。
3 文字ごとに 4 つの SVG マスクを制御します。8 つの例を挙げると:
灰色部分は各 SVG の透明な位置であり、各 SVG の位置が正しい場合、最終的にはこの効果が得られるはずです:
幸いなことに、SVG は dataurl の形式で背景画像として使用されていますが、この要素の高さは固定されているため、背景のさまざまな塗りつぶし方法(cover
/contain
など)を考慮する必要はありません。言い換えれば、正しい Flag を解出するためには、SVG の最終的なtop
値はその透明な位置の高さにのみ依存する必要があります。
解法#
私は各 details タグと SVG コンテナの高さがどのように変化するかを見ていませんでした。これは本当に多すぎます(もしかしたら Typed OM を使って分析することができるかもしれません?)。
しかし、各 SVG は 3 文字を制御しているため、最終的には 27^3 のすべての可能性を試す必要があります。直接暴力的な反復方法で解決することにしました。
ここで問題が発生します:SVG が正しい位置に到達したかどうかをどうやって知るか?
同級生はヘッドレスブラウザを使って画像マッチングを試みましたが、明らかにこれはうまくいかず、効率が非常に低く、各 SVG を隔離して正しく認識する必要があります。
私が考えたのは、各 SVG のtop
値を取得できれば、透明な位置の高さを計算し、期待される高さと比較できるので、SVG が正しい位置に到達したことを確認できるということです。
これは実際には簡単で、以下の手順で行います:
- 現在の 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
}
- 各 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]
- 各 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
でも取得できたので、遠回りをしてしまいました
- どの文字を変更するか?
これは最も重要な部分です。特定の文字を変更することで SVG のtop
値を変更し、SVG を移動させる目的を達成する必要があります。
しかし、このウェブページは完全に CSS で実装されているため、直接文字を変更することは不可能です。
では、赤い上下矢印をシミュレートしてクリックすることはできるでしょうか?それも無理です。ウェブページは各矢印の位置に 26 個の details タグを重ねており、各クリック時に赤い矢印の位置が異なる details を操作できるようにオフセットを設定しています。
したがって、details タグの属性を直接操作することを試みる必要があります。details が開かれると、その要素自体にopen
属性が付与されます。この属性を操作することで、details タグを開いたり閉じたりする効果を実現できます。
規則を探ると、最後から 1 番目の detail が開かれると文字はa
、最後から 2 番目の 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);