记录使用rust生成wasm文件全过程

协议分析 · 昨天 · 11 人浏览

说明

在网站开发中,接口的安全是重中之重,有些付费接口,防止被别人拿去重发等。
有一个方式来保护接口还是非常有必要的。

本来考虑的是JS,但JS加密太弱了,后来采用的是TypeScript生成的wasm,虽然可以生成了wasm,缺点也很明显,能防一下小白。但高手就很难防的住了。

所以采用了Rust语言来生成wasm。

安装

我用的是Visual Studio 2019,第一步,首先是勾选上:使用 C++ 的桌面开发,安装完成之后。
访问官网:https://rustup.rs/下载rustup-init.exe,然后一路默认即可。
如果不安装:使用 C++ 的桌面开发,会提示link错误。

1、创建库命令:

cargo new --lib secure_core
cd secure_core

2、打开目录下的 Cargo.toml 文件,将其替换为以下内容:

[package]
name = "secure_core"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"] # 告诉编译器我们要输出 C 规范的动态库 (WASM 需要)

[dependencies]
wasm-bindgen = "0.2"    # Rust 与 JS 交互的桥梁
js-sys = "0.3"          # 允许在 Rust 中调用 JS 的原生 API (如 Date.now)
obfstr = "0.4"          # 核心:编译期字符串混淆宏!

[dependencies.web-sys]
version = "0.3"
features = [
    "Window",
    "Navigator",
    "Document",
    "HtmlCanvasElement",
    "Location"
]

3、打开 src/lib.rs

use wasm_bindgen::prelude::*;
use js_sys::{Date, Math, Reflect};
use obfstr::obfstr; 

const M: [usize; 40] = [
    0x21, 0x20, 0x05, 0x0E, 0x1C, 0x09, 0x27, 0x02, 0x15, 0x0B,
    0x28, 0x07, 0x16, 0x12, 0x01, 0x22, 0x0A, 0x19, 0x24, 0x0F,
    0x03, 0x1D, 0x0C, 0x25, 0x14, 0x06, 0x1F, 0x1A, 0x13, 0x08,
    0x18, 0x21, 0x0D, 0x26, 0x1B, 0x04, 0x23, 0x1E, 0x17, 0x20
];

fn get_key_bytes(hex_str: &str) -> Vec<u8> {
    (0..hex_str.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex_str[i..i + 2], 16).unwrap_or(0))
        .collect()
}

fn djb2_hash(data: &str) -> u32 {
    let mut hash: u32 = 5381;
    for byte in data.bytes() {
        hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
    }
    hash
}

fn check_safe_environment(window: &web_sys::Window) -> bool {
    let global = js_sys::global();
    if let Ok(has_process) = Reflect::has(&global, &JsValue::from_str("process")) {
        if has_process { return false; } 
    }

    let navigator = window.navigator();

    if let Ok(webdriver_val) = Reflect::get(&navigator, &JsValue::from_str("webdriver")) {
        if webdriver_val.is_truthy() {
            return false;
        }
    }

    let hardware_concurrency = navigator.hardware_concurrency();
    if hardware_concurrency == 0.0 {
        return false;
    }

    if let Some(document) = window.document() {
        if let Ok(canvas) = document.create_element("canvas") {
            if Reflect::has(&canvas, &JsValue::from_str("toDataURL")).is_err() {
                return false;
            }
        } else {
            return false;
        }
    }

    let hostname = window.location().hostname().unwrap_or_else(|_| "".to_string());
    if hostname != "cz.lxjc.com" {
        return false;
    }

    true
}

#[wasm_bindgen]
pub fn pack(d: &str) -> String {
    if d.is_empty() {
        return String::new();
    }

    let window = match web_sys::window() {
        Some(w) => w,
        None => return "41850692d5732be0140cb164a2bc671f".to_string(),
    };

    if !check_safe_environment(&window) {
        return "41850692d5732be0140cb164a2bc671f".to_string();
    }

    let key_bytes = get_key_bytes(obfstr!("3851094726148290537692751830464068259137"));
    
    let current_ms = Date::now();
    let nonce = format!("{:08x}", (Math::random() * 4294967295.0) as u32);
    
    let time_window = (current_ms / 60000.0) as u64;
    
    let token_seed = format!("SUPER_SECRET_{}_{}_{}", obfstr!("3851"), time_window, nonce);
    let local_token = format!("{:08x}", djb2_hash(&token_seed));
    
    let base_payload = format!("{}|{}|{}", d, nonce, local_token);
    let checksum = format!("{:08x}", djb2_hash(&base_payload));
    let payload = format!("{}|{}", base_payload, checksum);
    let raw = payload.as_bytes();

    let pad_len = 20 - (raw.len() % 20);
    let mut padded = raw.to_vec();
    padded.resize(raw.len() + pad_len, pad_len as u8);

    let mut iv: u32 = 0x5C;
    let blocks = padded.len() / 20;

    for b in 0..blocks {
        let offset = b * 20;
        let mut nibbles = vec![0u8; 40];
        
        for i in 0..20 {
            nibbles[i * 2] = (padded[offset + i] >> 4) & 0x0F;
            nibbles[i * 2 + 1] = padded[offset + i] & 0x0F;
        }

        let mut permuted = vec![0u8; 40];
        for i in 0..40 {
            let idx = M[i] - 1; 
            permuted[i] = nibbles[idx];
        }

        for i in 0..20 {
            padded[offset + i] = (permuted[i * 2] << 4) | permuted[i * 2 + 1];
        }

        for i in 0..20 {
            let mut p = padded[offset + i] as u32;

            p = p ^ iv;
            p = (p + key_bytes[i] as u32) & 0xFF;
            p = ((p << 3) | (p >> 5)) & 0xFF;
            
            let magic = (0xA3 + i as u32 * 17) & 0xFF;
            p = p ^ magic;
            p = p ^ key_bytes[19 - i] as u32;

            padded[offset + i] = p as u8;
            iv = p;
        }
    }

    let mut out = String::with_capacity(padded.len() * 2);
    for b in padded {
        out.push_str(&format!("{:02x}", b));
    }

    out
}

#[wasm_bindgen]
pub fn unpack_response(hex_str: &str) -> String {
    if hex_str.is_empty() || hex_str.len() % 2 != 0 {
        return String::new();
    }

    let rk: [u8; 16] = [
        0x75, 0x28, 0x30, 0x46, 0x40, 0x68, 0x25, 0x91, 
        0x37, 0x38, 0x52, 0x09, 0x47, 0x26, 0x14, 0x82
    ];

    let mut bytes = Vec::new();
    for i in (0..hex_str.len()).step_by(2) {
        if let Ok(b) = u8::from_str_radix(&hex_str[i..i+2], 16) {
            bytes.push(b);
        } else {
            return String::new();
        }
    }

    let mut state: u32 = 0x4B3C2D1E;
    let mut plain = Vec::with_capacity(bytes.len());

    for i in 0..bytes.len() {
        state = state.wrapping_mul(214013).wrapping_add(2531011);
        let rand_byte = ((state >> 16) & 0xFF) as u8;
        let key_modifier = rk[i % 16];

        let mut c = bytes[i];
        c = c.wrapping_sub(i as u8);
        let p = c ^ rand_byte ^ key_modifier;
        plain.push(p);
    }

    String::from_utf8(plain).unwrap_or_else(|_| String::new())
}

然后通过CMD输入:cd C:\Users\Administrator\secure_core
再执行:wasm-pack build --target web

即可在目录下,生成了pkg文件。这就是混淆之后的wasm文件。

引用

index.php

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WASM 加解密完整闭环测试</title>
    <style>
        body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; }
        textarea { width: 100%; height: 80px; margin-bottom: 10px; padding: 10px; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; border-radius: 4px; }
        button:hover { background: #0056b3; }
        .box { margin-top: 20px; padding: 15px; background: #f8f9fa; border: 1px solid #ddd; word-wrap: break-word; border-radius: 4px; }
        .success { color: green; font-weight: bold; }
        .error { color: red; font-weight: bold; }
    </style>
</head>
<body>

    <h2>前端加密 -> 后端解密 闭环测试</h2>
    
    <textarea id="dataInput" placeholder="输入需要保护的明文 (例如: user_id=123&action=login)"></textarea>
    <button id="encryptBtn">加密并发送给后端</button>

    <div class="box">
        <strong>前端生成的密文 (Hex):</strong>
        <div id="cipherResult" style="color: #666; font-size: 0.9em; margin-top: 5px;">等待操作...</div>
    </div>

    <div class="box">
        <strong>后端 PHP 返回的解密结果:</strong>
        <div id="serverResult" style="margin-top: 5px;">等待服务器响应...</div>
    </div>

    <script type="module">
    import init, { pack, unpack_response } from './pkg/secure_core.js';

    async function run() {
        try {
            await init({ module_or_path: './pkg/secure_core_bg.wasm' }); 
            console.log("引擎加载成功!");
        } catch (err) {
            alert("WASM 初始化失败: " + err);
            return;
        }

        document.getElementById('encryptBtn').addEventListener('click', async () => {
            const rawData = document.getElementById('dataInput').value.trim();
            if (!rawData) {
                alert("请输入数据");
                return;
            }

            let encryptedHex;
            try {
                encryptedHex = pack(rawData);
            } catch (err) {
                alert("加密过程崩溃: " + err);
                return;
            }
            
            document.getElementById('cipherResult').innerText = encryptedHex;
            document.getElementById('serverResult').innerText = "正在发送请求中...";

            try {
                const response = await fetch('jiemi.php', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ payload: encryptedHex })
                });

                const resObject = await response.json();
                const resultDiv = document.getElementById('serverResult');
                
                if (resObject && resObject.payload) {
                    let decryptedResponseText;
                    try {
                        decryptedResponseText = unpack_response(resObject.payload);
                    } catch(decErr) {
                        resultDiv.innerHTML = `<span class="error">前端解密回程失败:</span> ${decErr}`;
                        return;
                    }

                    const resData = JSON.parse(decryptedResponseText);

                    if (resData.code === 200) {
                        resultDiv.innerHTML = `<span class="success">成功提取明文:</span> ${resData.data}`;
                    } else {
                        resultDiv.innerHTML = `<span class="error">验证失败 [${resData.code}]:</span> ${resData.msg}`;
                    }
                } else {
                    resultDiv.innerHTML = `<span class="error">异常响应:</span> 后端未返回合法密文`;
                }

            } catch (err) {
                console.error(err);
                document.getElementById('serverResult').innerHTML = `<span class="error">请求后端失败,请检查网络</span>`;
            }
        });
    }

    run();
</script>
</body>
</html>

解密:
jiemi.php

<?php

class WasmCrypto
{
    private static $m = [
        0x21, 0x20, 0x05, 0x0E, 0x1C, 0x09, 0x27, 0x02, 0x15, 0x0B,
        0x28, 0x07, 0x16, 0x12, 0x01, 0x22, 0x0A, 0x19, 0x24, 0x0F,
        0x03, 0x1D, 0x0C, 0x25, 0x14, 0x06, 0x1F, 0x1A, 0x13, 0x08,
        0x18, 0x21, 0x0D, 0x26, 0x1B, 0x04, 0x23, 0x1E, 0x17, 0x20
    ];

    private static $k = "3851094726148290537692751830464068259137";

    private static $rk = [
        0x75, 0x28, 0x30, 0x46, 0x40, 0x68, 0x25, 0x91, 
        0x37, 0x38, 0x52, 0x09, 0x47, 0x26, 0x14, 0x82
    ];

    private static function getKeyBytes()
    {
        $kb = [];
        for ($i = 0; $i < 20; $i++) {
            $kb[] = hexdec(substr(self::$k, $i * 2, 2));
        }
        return $kb;
    }

    public static function djb2_hash($str) 
    {
        $hash = 5381;
        for ($i = 0, $len = strlen($str); $i < $len; $i++) {
            $hash = (($hash << 5) + $hash + ord($str[$i])) & 0xFFFFFFFF; 
        }
        return sprintf("%08x", $hash);
    }

    public static function unpack($hexString)
    {
        $hexString = trim($hexString); 
        if (empty($hexString) || strlen($hexString) % 2 !== 0) {
            return false;
        }

        $len = strlen($hexString) / 2;
        $dec = [];
        for ($i = 0; $i < $len; $i++) {
            $dec[] = hexdec(substr($hexString, $i * 2, 2));
        }

        $keyBytes = self::getKeyBytes();
        $iv = 0x5C;
        $blocks = $len / 20;

        for ($b = 0; $b < $blocks; $b++) {
            $offset = $b * 20;

            for ($i = 0; $i < 20; $i++) {
                $c = $dec[$offset + $i];
                $p = $c;

                $p = $p ^ $keyBytes[19 - $i];
                $magic = (0xA3 + $i * 17) & 0xFF;
                $p = $p ^ $magic;
                
                $p = (($p >> 3) | ($p << 5)) & 0xFF;
                $p = ($p + 256 - $keyBytes[$i]) & 0xFF;
                $p = $p ^ $iv;

                $iv = $c;
                $dec[$offset + $i] = $p;
            }

            $nibbles = array_fill(0, 40, 0);
            for ($i = 0; $i < 20; $i++) {
                $nibbles[$i * 2] = ($dec[$offset + $i] >> 4) & 0x0F;
                $nibbles[$i * 2 + 1] = $dec[$offset + $i] & 0x0F;
            }

            $unpermuted = array_fill(0, 40, 0);
            for ($i = 0; $i < 40; $i++) {
                $idx = self::$m[$i] - 1;
                $unpermuted[$idx] = $nibbles[$i];
            }

            for ($i = 0; $i < 20; $i++) {
                $dec[$offset + $i] = ($unpermuted[$i * 2] << 4) | $unpermuted[$i * 2 + 1];
            }
        }

        $padLen = end($dec);
        if ($padLen > 0 && $padLen <= 20) {
            $dec = array_slice($dec, 0, count($dec) - $padLen);
        }

        $payloadStr = '';
        foreach ($dec as $byte) {
            $payloadStr .= chr($byte);
        }

        return $payloadStr;
    }

    public static function response_pack($plainText)
    {
        $bytes = array_values(unpack('C*', $plainText));
        $len = count($bytes);
        
        $state = 0x4B3C2D1E;
        $out = [];

        for ($i = 0; $i < $len; $i++) {
            $state = ($state * 214013 + 2531011) & 0xFFFFFFFF;
            $rand_byte = ($state >> 16) & 0xFF;
            $key_modifier = self::$rk[$i % 16];

            $c = $bytes[$i] ^ $rand_byte ^ $key_modifier;
            $c = ($c + $i) & 0xFF;

            $out[] = sprintf("%02x", $c);
        }
        return implode('', $out);
    }
}

header('Content-Type: application/json; charset=utf-8');

$jsonInput = file_get_contents('php://input');
$requestData = json_decode($jsonInput, true);

$encryptedHex = isset($requestData['payload']) ? $requestData['payload'] : '';

if (empty($encryptedHex)) {
    die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 400, "msg" => "缺失加密数据"]))]));
}

$decryptedStr = WasmCrypto::unpack($encryptedHex);

if ($decryptedStr !== false) {
    $parts = explode('|', $decryptedStr);
    
    if (count($parts) < 4) {
        die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 403, "msg" => "非法请求:数据格式损坏或来源不受信任"]))]));
    }

    $checksum_from_client = array_pop($parts);
    $local_token_from_client = array_pop($parts);
    $nonce = array_pop($parts);
    
    $realData = implode('|', $parts);
    
    $basePayload = $realData . '|' . $nonce . '|' . $local_token_from_client;
    $calculated_checksum = WasmCrypto::djb2_hash($basePayload);
    
    if ($calculated_checksum !== $checksum_from_client) {
        die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 403, "msg" => "非法请求:数据校验失败,涉嫌篡改!"]))]));
    }

    $now = microtime(true) * 1000; 
    
    $current_window = floor($now / 60000);
    $previous_window = $current_window - 1;
    $next_window = $current_window + 1;

    $secret_prefix = "SUPER_SECRET_3851_"; 
    
    $expected_token_current = WasmCrypto::djb2_hash($secret_prefix . $current_window . '_' . $nonce);
    $expected_token_prev = WasmCrypto::djb2_hash($secret_prefix . $previous_window . '_' . $nonce);
    $expected_token_next = WasmCrypto::djb2_hash($secret_prefix . $next_window . '_' . $nonce);

    if (
        $local_token_from_client !== $expected_token_current && 
        $local_token_from_client !== $expected_token_prev && 
        $local_token_from_client !== $expected_token_next
    ) {
        die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 403, "msg" => "非法请求:动态环境令牌失效"]))]));
    }

    $responseData = json_encode([
        "code" => 200, 
        "msg" => "验证通过", 
        "data" => $realData
    ], JSON_UNESCAPED_UNICODE);

    echo json_encode(["payload" => WasmCrypto::response_pack($responseData)]);

} else {
    $errorData = json_encode(["code" => 403, "msg" => "非法请求:解密彻底失败"], JSON_UNESCAPED_UNICODE);
    echo json_encode(["payload" => WasmCrypto::response_pack($errorData)]);
}