HGAME-2026-WEEK2-MISC

54次阅读
没有评论

WEEK2-MISC

VidarToken

HGAME-2026-WEEK2-MISC

长得有点像web3钱包

[00:16:00] 200 -    2KB - /app.js
[00:16:03] 200 -    1KB - /index.html
[00:16:06] 400 -    43B - /rpc/
[00:16:06] 400 -     0B - /rpcwithcert/
const rpcUrl = `${window.location.origin}/rpc`;
const vaultStatusEl = document.getElementById("vault-status");
const walletStatusEl = document.getElementById("wallet-status");
const appContentEl = document.getElementById("app-content");

let entranceAddress = null;
let walletProvider = null;

vaultStatusEl.textContent = "请先连接 (。•̀ᴗ•́。)";

function readCString(mem, offset, max = 128) {
  const bytes = new Uint8Array(mem.buffer, offset, max);
  let out = "";
  for (let i = 0; i < bytes.length; i++) {
    if (bytes[i] === 0) break;
    out += String.fromCharCode(bytes[i]);
  }
  return out;
}

async function connectWallet() {
  walletProvider = null;
  walletStatusEl.classList.remove("active");
  appContentEl.classList.add("locked");
  appContentEl.classList.remove("unlocked");
  vaultStatusEl.textContent = "浏览器钱包在非 HTTPS 环境无法直接连接 (>﹏<)";
}

async function checkEligibility() {
  vaultStatusEl.textContent = "尝试读取元数据... (。•̀ᴗ•́。)";
  try {
    if (!entranceAddress) {
      const res = await fetch("/wasm/k.wasm", { method: "GET" });
      if (res.ok) {
        const wasm = await res.arrayBuffer();
        const { instance } = await WebAssembly.instantiate(wasm, {});
        const ptr = instance.exports.get_entrance();
        const text = readCString(instance.exports.memory, ptr, 80);
        const match = text.match(/ENTRANCE=(0x[a-fA-F0-9]{40})/);
        entranceAddress = match ? match[1] : "";
      }
    }

    if (!entranceAddress) {
      vaultStatusEl.textContent = "入口未就绪 (´• ω •`)";
      return;
    }

    const provider = new ethers.JsonRpcProvider(rpcUrl);
    const vault = new ethers.Contract(
      entranceAddress,
      ["function tokenURI(uint256) view returns (string)"],
      provider
    );

    await vault.tokenURI(0);
    vaultStatusEl.textContent = "元数据已就绪 (。•̀ᴗ•́。)";
  } catch (err) {
    vaultStatusEl.textContent = "读取失败 (。•́︿•̀。)";
  }
}

function safe(fn) {
  return async () => {
    try {
      await fn();
    } catch (err) {
      if (vaultStatusEl) vaultStatusEl.textContent = err.message || String(err);
    }
  };
}

document.getElementById("connect-wallet").addEventListener("click", safe(connectWallet));
const checkBtn = document.getElementById("check-eligibility");
if (checkBtn) {
  checkBtn.disabled = true; //页面交互禁用
  checkBtn.setAttribute("aria-disabled", "true");
}

这段js有问题啊,页面是点不了的,还会弹伪报错。
流程是这样的:加载/wasm/k.wasm 随后调用其导出函数get_entrance() 从内存中读取ENTRANCE=(0x[a-fA-F0-9]{40})得到合约地址 随后使用ethers.js 连接到/rpc 调用合约tokenURI(0)

先获取合约地址

// 1. 定义读取内存字符串的辅助函数(原代码中的函数)
function readCString(mem, offset, max = 128) {
  const bytes = new Uint8Array(mem.buffer, offset, max);
  let out = "";
  for (let i = 0; i < bytes.length; i++) {
    if (bytes[i] === 0) break;
    out += String.fromCharCode(bytes[i]);
  }
  return out;
}

// 2. 手动加载 WASM 并提取地址
fetch("/wasm/k.wasm")
  .then(res => res.arrayBuffer())
  .then(async wasm => {
    const { instance } = await WebAssembly.instantiate(wasm, {});
    const ptr = instance.exports.get_entrance();
    const text = readCString(instance.exports.memory, ptr, 80);
    console.log("WASM返回的原始文本:", text);
    
    const match = text.match(/ENTRANCE=(0x[a-fA-F0-9]{40})/);
    if (match) {
        console.log("拿到合约地址了:", match[1]);
        // 把地址存到全局变量,方便下一步使用
        window.targetAddress = match[1];
    } else {
        console.error("未找到地址格式");
    }
  });

HGAME-2026-WEEK2-MISC

WASM返回的原始文本: ENTRANCE=0x39529fdA4CbB4f8Bfca2858f9BfAeb28B904Adc0

接下来模拟tokenURI调用

async function solve() {
    const contractAddress = "0x39529fdA4CbB4f8Bfca2858f9BfAeb28B904Adc0";
    const rpcUrl = "/rpc"; // 题目中的相对路径

    // 1. 构造 tokenURI(0) 的调用数据
    // tokenURI(uint256) 的函数签名哈希是 c87b56dd
    // 参数 0 需要补齐为 64 位的 16 进制
    const functionSelector = "0xc87b56dd"; 
    const parameter = "0000000000000000000000000000000000000000000000000000000000000000";
    const data = functionSelector + parameter;

    console.log("正在发送 RPC 请求...");

    // 2. 发送 eth_call 请求
    const response = await fetch(rpcUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            jsonrpc: "2.0",
            method: "eth_call",
            params: [
                {
                    to: contractAddress,
                    data: data
                },
                "latest"
            ],
            id: 1
        })
    });

    const json = await response.json();
    
    if (json.error) {
        console.error("RPC 报错:", json.error);
        return;
    }

    const rawHex = json.result;
    console.log("RPC 返回原始 Hex:", rawHex);

    // 3. 解码返回的字符串
    // 返回数据格式通常是 ABI 编码:[32字节偏移量] [32字节长度] [字符串内容...]
    // 我们直接粗暴一点,把 Hex 转成 ASCII 并提取可打印字符即可看到 Flag
    let str = "";
    // 去掉开头的 0x
    const cleanHex = rawHex.replace("0x", "");
    for (let i = 0; i < cleanHex.length; i += 2) {
        const charCode = parseInt(cleanHex.substr(i, 2), 16);
        // 过滤出可打印字符 (Flag通常在这里面)
        if (charCode >= 32 && charCode <= 126) {
            str += String.fromCharCode(charCode);
        }
    }

    console.log("%c▼ ▼ ▼ 最终结果 ▼ ▼ ▼", "color: lime; font-size: 14px;");
    console.log(str);
    alert("解码结果: " + str);
}

solve();

HGAME-2026-WEEK2-MISC

{"name":"VidarPunks #0","description":"VidarPunks Vault NFT. Seek your fortune with VidarCoin.","attributes":[{"trait_type":"Linked Coin Address","value":"0xc5273abfb36550090095b1edec019216ad21be6c"}],"vidar_coin":"0xc5273abfb36550090095b1edec019216ad21be6c"}

我们得到了一个vidar_coin 地址

async function digVidarCoin() {
    // 这是你 JSON 里解出的新地址
    const targetAddress = "0xc5273abfb36550090095b1edec019216ad21be6c";
    const rpcUrl = "/rpc"; 

    console.log(`正在挖掘合约 ${targetAddress} 的内部存储...`);

    // 辅助函数:十六进制转字符串
    function hexToString(hex) {
        let str = "";
        hex = hex.replace(/^0x/, "");
        for (let i = 0; i < hex.length; i += 2) {
            const code = parseInt(hex.substr(i, 2), 16);
            if (code >= 32 && code <= 126) { // 只显示可打印字符
                str += String.fromCharCode(code);
            }
        }
        return str;
    }

    // 遍历读取前 15 个存储槽 (Slot 0 - Slot 14)
    for (let i = 0; i < 15; i++) {
        // 构造 eth_getStorageAt 请求
        // 参数:[地址, 槽位索引(hex), 区块高度]
        const slotHex = "0x" + i.toString(16);
        
        try {
            const response = await fetch(rpcUrl, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    jsonrpc: "2.0",
                    method: "eth_getStorageAt",
                    params: [targetAddress, slotHex, "latest"],
                    id: i
                })
            });
            const json = await response.json();
            const rawData = json.result;

            // 尝试解码看看有没有像 Flag 的东西
            const decoded = hexToString(rawData);
            
            console.log(`[Slot ${i}] Raw: ${rawData}`);
            if (decoded.length > 0) {
                console.log(`%c   >>> 解码尝试: ${decoded}`, "color: yellow");
            }
            
            // 如果发现 Flag 格式,弹窗提示
            if (decoded.includes("Vidar{") || decoded.includes("flag{")) {
                console.log(`%c🎉 找到 Flag 了!`, "font-size: 20px; color: lime");
                alert("Flag found in Slot " + i + ":\n" + decoded);
                return;
            }

        } catch (e) {
            console.error(`读取 Slot ${i} 失败`, e);
        }
    }
    console.log("挖掘完成。如果没看到完整 Flag,可能Flag被拆分在多个Slot里,或者需要拼接。");
}

digVidarCoin();

HGAME-2026-WEEK2-MISC

接下来 调 tokenURI(0):NFT 元数据里给出下一跳 Coin 地址

对 entrance 调 tokenURI(0),解出 JSON:

{
  "name":"VidarPunks #0",
  "vidar_coin":"0xc5273abfb36550090095b1edec019216ad21be6c",
  "attributes":[{"trait_type":"Linked Coin Address","value":"0xc5273abf...be6c"}]
}

这一步对应题面 “Punks and Coins collide”:Punks(NFT) 指向 Coin 合约,让你去挖 Coin。

枚举 Coin 合约:看起来像 ERC20,但 symbol 很异常

对 Coin 地址做常见 ERC20 查询:

  • name() = VidarCoin
  • decimals() = 26
  • totalSupply() = 0
  • logs = 0
  • symbol()返回一长串 hex 字符串0x6960606a...607a

这些现象组合起来基本说明:

  • 它不是正常代币,像“谜题容器”
  • symbol 被故意改造成“密文载体”

从 bytecode 提取 selector:发现 3 个自定义函数,但全都空 revert

你从 eth_getCode​ 扫 PUSH4,得到 12 个 selector:

9 个是标准 ERC20,另外 3 个自定义:

  • 0x64656600
  • 0xc4a11628
  • 0x43000821

对这三个 selector 做各种参数和 from​(包括 ownerOf(0) 的 owner、entrance、coin 自己)测试,结果始终是:

  • execution reverted​ 且 data: 0x​(空 revert reason

这一步结论是:自定义函数不是直接给答案的路径(至少在现有条件下完全不可用),更像干扰/门槛

改走“链上取证”:读 storage,确认 symbol 密文来自动态字符串存储

你扫 eth_getStorageAt 发现:

  • slot3​/slot4​ 是短字符串(能看出 VidarCoin​、VIDAR
  • slot5 = ...0059

这里 0x59​ 的最低位是 1,符合 Solidity 对 动态 bytes/string “短编码/长编码” 的约定:
当 slot 存的是 “2*len+1” 时表示长字符串,长度为 (0x59-1)/2 = 44 字节。

而你 symbol​ 的密文刚好是 44 bytes,于是可以断定:flag(或密文)被当作动态字符串/bytes 存在 storage

keccak256(slot) 位置把 44 bytes 原样读出来(绕过 symbol 任何加工)

动态字符串真实数据存放在:

  • base = keccak256(pad32(slotIndex))
  • base 开始按 32 字节 word 读取

你用 web3_sha3(pad32(5)) 得到:

  • base = 0x036b...3db0

然后:

  • word0​ + word1​ 拼起来正好是那 44 bytes 密文(完全对上 symbol()

这一步非常关键:你拿到的是链上原始密文,不再依赖合约函数返回如何编码

利用已知 flag 前缀 hgame{ 反推 XOR key,得到最终 flag

const hex = "0x6960606a647c542a3545726848725534307e5e4c4f487658445562587647726a2c5d5b7d303737333537607a";

function hexToBytes(h){
  h = h.startsWith("0x") ? h.slice(2) : h;
  const out = new Uint8Array(h.length/2);
  for(let i=0;i<out.length;i++) out[i]=parseInt(h.slice(i*2,i*2+2),16);
  return out;
}
function bytesToStr(b){ return new TextDecoder().decode(b); }

function addMod256(b, k){
  const o=new Uint8Array(b.length);
  for(let i=0;i<b.length;i++) o[i]=(b[i]+k)&255;
  return o;
}
function xorByte(b, k){
  const o=new Uint8Array(b.length);
  for(let i=0;i<b.length;i++) o[i]=b[i]^k;
  return o;
}
function xorRepeat(b, keyStr){
  const key = new TextEncoder().encode(keyStr);
  const o=new Uint8Array(b.length);
  for(let i=0;i<b.length;i++) o[i]=b[i]^key[i%key.length];
  return o;
}
// rot95 on printable ASCII (0x20..0x7e); keep others unchanged
function rot95(b, shift){
  const o=new Uint8Array(b.length);
  for(let i=0;i<b.length;i++){
    const c=b[i];
    if (c>=0x20 && c<=0x7e){
      const pos=c-0x20;
      o[i]=0x20 + ((pos+shift)%95+95)%95;
    } else o[i]=c;
  }
  return o;
}
function interesting(s){
  return /flag\{|ctf\{|FLAG\{|[{}]/.test(s);
}

(() => {
  const b = hexToBytes(hex);
  const keys = ["VIDAR", "VidarCoin"];

  // 1) mod256 shifts
  for (let k=-64;k<=64;k++){
    const s = bytesToStr(addMod256(b,k));
    if (interesting(s)) console.log("[addMod256]", k, s);
  }

  // 2) rot95 shifts (printable)
  for (let k=-94;k<=94;k++){
    const s = bytesToStr(rot95(b,k));
    if (interesting(s)) console.log("[rot95]", k, s);
  }

  // 3) single-byte xor
  for (let k=0;k<256;k++){
    const s = bytesToStr(xorByte(b,k));
    if (interesting(s)) console.log("[xor]", k, s);
  }

  // 4) xor repeating keys
  for (const key of keys){
    const s = bytesToStr(xorRepeat(b,key));
    if (interesting(s)) console.log("[xorRepeat]", key, s);
  }
})();

先做了暴力(rot95、单字节 XOR),看到单字节 XOR=7 时已经出现了 ...{...} 结构,这提示“确实是 XOR 类加密”。

但你补充规则:flag 必须以 hgame{ 开头。于是做已知明文攻击:

  • 密文第 1 字节 0x69​ 应该解成 'h'(0x68)​ ⇒ key[0] = 0x69 ^ 0x68 = 0x01
  • 密文第 2 字节 0x60​ 应该解成 'g'(0x67)​ ⇒ key[1] = 0x60 ^ 0x67 = 0x07
  • 再验证第 3/4 字节也符合交替 0x01,0x07

因此 key 是 **2 字节循环 XOR:**​ [0x01, 0x07]交替

用该 key 解出明文,得到最终 flag:

hgame{U-4BsoIuT31y_KNOw_ERc_w@sm-ZZz106440a}

Invest on Matrix

本质是其实是在问二维码的有效数据区块位置,但我懒得搞了,直接买到了能扫出来为止

最右下角的那个区域是不必要做出来的

import numpy as np
from PIL import Image, ImageOps

# ==============================================================================
# 1. 数据整合区 (包含了你提供的所有数据)
# ==============================================================================

# --- 左上角定位 ---
hint_01 = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1]
hint_02 = [1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1]
hint_06 = [1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0]
hint_07 = [0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]

# --- 右上角定位 ---
hint_04 = [0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0]
hint_05 = [1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1]
hint_09 = [1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0]
hint_10 = [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1]

# --- 左下角定位 ---
hint_16 = [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
hint_17 = [0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0]
hint_21 = [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1]
hint_22 = [0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1]

# --- 核心数据区 (你新买的) ---
hint_08 = [0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]
hint_12 = [1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0]
hint_13 = [1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0]
hint_14 = [1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0]
hint_18 = [1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1]

# --- 缺失部分 ---
hint_03, hint_11, hint_15, hint_19, hint_20, hint_23, hint_24, hint_25 = [0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0], [1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1], [], [], [], [], []

# ==============================================================================
# 2. 生成逻辑
# ==============================================================================

def solve_qr_final():
    hints_map = {
        1: hint_01, 2: hint_02, 3: hint_03, 4: hint_04, 5: hint_05,
        6: hint_06, 7: hint_07, 8: hint_08, 9: hint_09, 10: hint_10,
        11: hint_11, 12: hint_12, 13: hint_13, 14: hint_14, 15: hint_15,
        16: hint_16, 17: hint_17, 18: hint_18, 19: hint_19, 20: hint_20,
        21: hint_21, 22: hint_22, 23: hint_23, 24: hint_24, 25: hint_25
    }

    # 1. 创建画布
    # 使用 255 (白色) 作为默认背景,这样即便有缺失,也不会干扰扫描器
    # (或者用灰色 128 看缺失位置,但为了扫描成功率,这里建议背景偏白或浅灰)
    matrix = np.full((25, 25), 255, dtype=np.uint8)

    # 2. 填充数据
    for pts, data in hints_map.items():
        if not data or len(data) != 25: 
            # 如果这块没买,我们把这块涂成灰色,提示缺失,但不要全黑
            idx = pts - 1
            y, x = (idx // 5) * 5, (idx % 5) * 5
            matrix[y:y+5, x:x+5] = 127
            continue
        
        block = np.array(data).reshape(5, 5)
        
        # 3. 颜色映射 (非常重要)
        # 题目数据: 1 (通常代表模块存在,即黑色), 0 (白色)
        # 图片数据: 0 是黑, 255 是白
        pixel_block = np.where(block == 1, 0, 255)

        idx = pts - 1
        y, x = (idx // 5) * 5, (idx % 5) * 5
        matrix[y:y+5, x:x+5] = pixel_block

    # 4. 转为图片并放大
    img = Image.fromarray(matrix)
    img = img.resize((500, 500), resample=Image.NEAREST)
    
    # 5. 【关键】增加白色边框 (Quiet Zone)
    # 二维码标准要求周围至少有 4 个模块宽度的白边
    img_final = ImageOps.expand(img, border=60, fill='white')
    
    print("✅ 终极图片生成完毕!")
    print("已添加白边,并修正了对比度。")
    img_final.show()
    img_final.save("flag_complete.png")

if __name__ == "__main__":
    solve_qr_final()

HGAME-2026-WEEK2-MISC

HGAME-2026-WEEK2-MISC

正文完
 0
评论(没有评论)