FEAT:🚩 Daily-AlpacaHack 「Renda」Medium
論理演算と算術演算の関係式を用いた復号
20260704-daily_alpaca-rev-medium-renda
Summary
本問は,アンチデバッグ機能を持つ難読化されたJSを解析する問題です.
[!info] Challenge Info
- Category: Rev
- Description: 連打するだけ!
- Tools & TechStack:
- Javascript
- JavaScript Deobfuscator
- Release: 2026/07/04
階層構造
1
2
3
.
└── index.html
1 directories, 1 files
ソースコードの調査
index.html の最上部には,Click 100,000 times to get the flag! と記載されているため,要素をクリックしまくれば Flag が入手できそうです.
index.html
1
2
3
4
5
6
Click!
<p id="cnt">count: 0</p>
<p id="flag">Click 100,000 times to get the flag!</p>
<script>//...</script>
難読化の解除とスクリプトの解析
今回は難読化の解除には,オンラインツールを使用しました.1
ツールに通した後,クリック判定処理のイベントハンドラを探すために,以下のキーワード検索を試しました.
100000という数値:ie5, 0x0186A0, 100000, 0303204- クリック判定とカウントに必要な関数:
addEventListener, getElementById
すると,ie5 と addEvent,GetEleme という部分的な結果でマッチングしました.
この部分がクリックを判定し,処理を行っているように見えるためコードを読み解きます.
また,この問題では問題サーバが与えられていないため,コード内に Flag 本体やそれに準じた情報が埋め込まれているはずです.
script タグ内の抜粋
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
//...
var X = 0;
// Click制御
// addEventListener
document["addEvent" + UN(-566, -551) + Uy(135, "0x7f")]("click", function () {
const hV = {I: "0x152", V: "0x165", N: "0xf8", y: "*(WB", p: "0x13e", l: "0x134", C: "0x11f", e: "%Lji", f: 291, L: "!flb", v: "0x110", A: "0x137", x: "0x124", q: "sE8V", Y: "0x25b", W: 592, a: 620, o: "0x24a", D: "0x130", S: "aQcR", b: 335, w: "0x15e", J: "0x103", Q: "1Oeu", d: 288, k: 265, z: "0xfc", E: "0x139", O: "0x13d", H: 379, M: 344, P: "0x27f", hN: "0x2a7", hy: 321, hp: "0x163", hl: 672, hC: "0x2b3", he: 377, hf: "0x15e", hL: "0x2a1", hv: "0x28a", hA: 669, hx: "0x2bd", hq: 340, hY: "0x140", hW: 306, ha: "1Oeu", ho: 222, hD: "xQ&)"}, hI = {I: "0x21c"}, hu = {I: 238}, hn = {I: 458};
const V = {};
V[UN(-hV.I, -hV.V - hu.I) + "E"] = "(((.+)+)+)+$", V.uajPd = u(-hV.N - -hn.I, hV.y), V[UN(-hV.p, -hV.l - hu.I) + "B"] = function (p, l) {
return p >= l;
}, V[u(-hV.C - -hn.I, hV.e) + "s"] = function (p, l) {
return p !== l;
}, V[u(-hV.f - -hn.I, hV.L) + "d"] = "EuyXL";
V.EqGum = UN(-hV.v, -hV.A - hu.I) + "c", V[u(-hV.x - -hn.I, hV.q) + "Z"] = Uy(hV.Y - hI.I, hV.W);
const N = V;
X++;
// getElementById?
document["getE" + Uy(hV.a - hI.I, hV.o) + "ntBy" + "Id"](N.uajPd)[u(-hV.D - -hn.I, hV.S) + UN(-hV.b, -hV.w - hu.I) + "ent"] = u(-hV.J - -hn.I, hV.Q) + UN(-hV.d, -hV.k - hu.I) + X;
if (N[u(-hV.z - -hn.I, hV.q) + "B"](X, 1e5)) {
if (N.buAys(N[UN(-hV.E, -hV.O - hu.I) + "d"], N.EqGum)) {
const p = [179, 35, 98, 94, 95, 30, 164, 201, 235, 121, 252, 24, 237, 123, 152, 243, 211, 114, 160, 14, 252, 65, 95, 170, 241, 136, 175, 59, 215, 37, 22, 241, 117, 198, 210, 195, 171, 121, 15, 117, 102, 80, 222, 15, 70, 249, 31, 239, 172, 184, 121, 201, 193], l = [245, 111, 35, 25, 101, 62, 229, 165, 155, 24, 159, 121, 150, 20, 234, 146, 188, 0, 193, 97, 142, 32, 48, 216, 144, 231, 221, 90, 184, 87, 119, 158, 7, 167, 189, 177, 202, 22, 125, 20, 86, 34, 191, 96, 52, 152, 112, 157, 205, 215, 11, 168, 188], C = l[UN(-hV.H, -hV.M - hu.I)]((e, f) => String["from" + UN(-303, -306 - hu.I) + Uy("0x278" - hI.I, "0x29b")](e ^ p[f])).join("");
document["getEleme" + Uy(hV.P - hI.I, hV.hN) + "Id"](N[UN(-hV.hy, -hV.hp - hu.I) + "Z"])[Uy(hV.hl - hI.I, hV.hC) + UN(-hV.he, -hV.hf - hu.I) + Uy(hV.hL - hI.I, hV.hv)] = C;
} else return N.toString().search(YaPbUp[Uy(hV.hA - hI.I, hV.hx) + "E"])["toSt" + UN(-hV.hq, -hV.hY - hu.I)]()[u(-hV.hW - -hn.I, hV.ha) + "truc" + "tor"](y).search("(((." + u(-hV.ho - -hn.I, hV.hD) + "+)+$");
}
});
暗号化アルゴリズムの調査
Flag の暗号化処理は以下のようになっていました.
p, lという同一長の配列を要素毎にXORを行い,文字に変換し結合する.
復元した暗号化アルゴリズム
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
document.addEventListener("click", function () {
X++;
// カウンタ表示の更新
document.getElementById(N.uajPd).textContent = "クリック数: " + X;
// Xが100000 (1e5) 以上になった場合の処理
if (X >= 100000) {
const p = [179, 35, 98, 94, 95, 30, 164, 201, 235, 121, 252, 24, 237, 123, 152, 243, 211, 114, 160, 14, 252, 65, 95, 170, 241, 136, 175, 59, 215, 37, 22, 241, 117, 198, 210, 195, 171, 121, 15, 117, 102, 80, 222, 15, 70, 249, 31, 239, 172, 184, 121, 201, 193];
const l = [245, 111, 35, 25, 101, 62, 229, 165, 155, 24, 159, 121, 150, 20, 234, 146, 188, 0, 193, 97, 142, 32, 48, 216, 144, 231, 221, 90, 184, 87, 119, 158, 7, 167, 189, 177, 202, 22, 125, 20, 86, 34, 191, 96, 52, 152, 112, 157, 205, 215, 11, 168, 188];
// map関数でXOR演算を行い,文字に変換して結合
const flagString = l.map((e, f) => String.fromCharCode(e ^ p[f])).join("");
// 指定された要素のtextContentまたはinnerHTMLに復号した文字列を代入
document.getElementById(N.EqGumZ).textContent = flagString;
}
});
復号
単純な排他的論理和なため,同じ演算を行うことで復号することができました.
decrypt.py
1
2
3
4
5
6
7
8
P = [179, 35, 98, 94, 95, 30, 164, 201, 235, 121, 252, 24, 237, 123, 152, 243, 211, 114, 160, 14, 252, 65, 95, 170, 241, 136, 175, 59, 215, 37, 22, 241, 117, 198, 210, 195, 171, 121, 15, 117, 102, 80, 222, 15, 70, 249, 31, 239, 172, 184, 121, 201, 193]
L = [245, 111, 35, 25, 101, 62, 229, 165, 155, 24, 159, 121, 150, 20, 234, 146, 188, 0, 193, 97, 142, 32, 48, 216, 144, 231, 221, 90, 184, 87, 119, 158, 7, 167, 189, 177, 202, 22, 125, 20, 86, 34, 191, 96, 52, 152, 112, 157, 205, 215, 11, 168, 188]
result = []
for i, el in enumerate(P):
result.append(el ^ L[i])
print(''.join(chr(c) for c in result))
別解: 変数書き換え
このプログラムには,自分自身のソースコードを正規表現でマッチングし,関数の構造が1行に詰まっている状況でなければ,起動を妨害するアンチデバッグ機能が付いていました.
しかし,元のソースコードと難読化解除後のソースコードを比較すること解析は可能です.
開発者ツールのコンソールを使用して,X = 99999; と書き換え,再度クリックすることでも,Flag 入手することができました.
これらの関数のコードリーディングには,Gemini Pro を使用しました.
Post-Mortem & Dead ends
難読化を解除した後に 1e5 を書き換えてワンクリックで Flag が表示できるようにしたのに,タブが無限ループ…
アンチデバッグ機能の回避に時間かかった… :(