Post

FEAT:🚩 Daily-AlpacaHack 「Renda」Medium

論理演算と算術演算の関係式を用いた復号

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

すると,ie5addEventGetEleme という部分的な結果でマッチングしました.
この部分がクリックを判定し,処理を行っているように見えるためコードを読み解きます.
また,この問題では問題サーバが与えられていないため,コード内に 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 が表示できるようにしたのに,タブが無限ループ…
アンチデバッグ機能の回避に時間かかった… :(

References

This post is licensed under CC BY 4.0 by the author.