Auto.js 入门指南(十五)脚本加密与安全防护

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第十五章:脚本加密与安全防护


一、理论讲解:脚本加密、混淆与安全防护的意义

1.1 为什么需要脚本加密与安全防护?

在 Auto.js 自动化脚本的开发与分发过程中,保护脚本的核心逻辑和敏感数据变得尤为重要。主要原因包括:

  • 防止逆向工程与盗用:保护原创思路和业务逻辑,避免他人直接复制或修改后发布。
  • 保护敏感信息:脚本中可能包含账号密码、API Keys、服务器地址等敏感数据,需要防止泄露。
  • 防止恶意篡改:确保脚本执行的完整性,防止被不法分子植入恶意代码。
  • 维护商业价值:对于付费脚本或商业用途的自动化解决方案,加密是其价值体现的重要保障。

1.2 Auto.js 脚本面临的安全威胁

  • 源码暴露.js 脚本文件是明文的,任何人拿到文件都可以直接阅读和理解其逻辑。
  • 敏感信息硬编码:将账号密码等直接写在脚本中,容易被提取。
  • 脚本篡改:恶意用户可能修改脚本行为,造成损失或滥用。
  • 调试器攻击:通过调试工具(如 Auto.js Pro 的调试功能、ADB 等)分析脚本运行时状态,绕过授权。
  • Hook 攻击:通过 Hook 技术修改 Auto.js 或 Android 系统的 API 行为,影响脚本正常运行。

1.3 加密、混淆与数字签名

  • 加密 (Encryption):将原始代码或数据通过算法转换为不可读的密文,只有拥有密钥才能解密。主要用于保护数据机密性。
  • 混淆 (Obfuscation):不改变代码原有功能的前提下,通过重命名变量、函数,插入冗余代码,打乱代码结构等方式,使其难以理解和分析。主要用于增加逆向难度。
  • 数字签名 (Digital Signature):通过密码学方法验证脚本的完整性和来源。可以判断脚本是否被篡改,以及是否来自可信作者。

1.4 密钥管理与安全策略

  • 密钥安全存储:密钥不能直接硬编码在脚本中,可以考虑通过网络获取、加密存储在本地、或者使用 Android Keystore 等系统级安全存储。
  • 密钥分发:如何安全地将密钥分发给授权用户。
  • 动态密钥:定期更换密钥,增加破解难度。
  • 多重防护:结合加密、混淆、授权校验、网络验证等多种手段,提升安全性。

二、代码示例:脚本加密与安全防护全场景实战

2.1 基础 Base64 加密/解密字符串

Base64 是一种编码方式,而非加密,但可用于简单的信息"隐藏"。

javascript 复制代码
// Base64 编码
function encodeBase64(str) {
    return java.util.Base64.getEncoder().encodeToString(java.lang.String(str).getBytes("UTF-8"));
}

// Base64 解码
function decodeBase64(str) {
    return new java.lang.String(java.util.Base64.getDecoder().decode(str), "UTF-8");
}

var secretData = "这是需要隐藏的敏感信息:账号123, 密码abc";
var encodedData = encodeBase64(secretData);
log("原始数据: " + secretData);
log("Base64编码: " + encodedData);

var decodedData = decodeBase64(encodedData);
log("Base64解码: " + decodedData);

// 实际应用:将编码后的字符串写入文件或作为脚本常量
files.write("/sdcard/encoded_info.txt", encodedData);
log("编码数据已保存到 /sdcard/encoded_info.txt");

2.2 使用 Java Crypto API 实现 AES 加密/解密

Auto.js 允许调用 Java 类,可以利用 Android 系统提供的加密库实现更强的加密。 注意:实际使用中,密钥管理和 IV(初始化向量)的生成与存储需要更严谨的处理。

javascript 复制代码
// 引入必要的 Java 类
var SecretKeySpec = Java.type("javax.crypto.spec.SecretKeySpec");
var Cipher = Java.type("javax.crypto.Cipher");
var Base64 = Java.type("android.util.Base64"); // 注意这里用 android.util.Base64

// AES 加密函数
function encryptAES(data, key) {
    try {
        var rawKey = key.getBytes("UTF-8");
        var skeySpec = new SecretKeySpec(rawKey, "AES");
        var cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // ECB模式简单,但安全性不如CBC
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        var encrypted = cipher.doFinal(data.getBytes("UTF-8"));
        return Base64.encodeToString(encrypted, Base64.NO_WRAP);
    } catch (e) {
        log("AES加密失败: " + e, "ERROR");
        return null;
    }
}

// AES 解密函数
function decryptAES(encryptedData, key) {
    try {
        var rawKey = key.getBytes("UTF-8");
        var skeySpec = new SecretKeySpec(rawKey, "AES");
        var cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        var original = cipher.doFinal(Base64.decode(encryptedData, Base64.NO_WRAP));
        return new java.lang.String(original, "UTF-8");
    } catch (e) {
        log("AES解密失败: " + e, "ERROR");
        return null;
    }
}

var aesKey = "ThisIsASecretKey"; // 16字节(128位)密钥
var originalText = "Auto.js核心算法数据";

var encryptedText = encryptAES(originalText, aesKey);
log("原始文本: " + originalText);
log("AES加密: " + encryptedText);

var decryptedText = decryptAES(encryptedText, aesKey);
log("AES解密: " + decryptedText);

// 敏感数据加密存储到文件
files.write("/sdcard/encrypted_data.txt", encryptedText);
log("加密数据已保存到 /sdcard/encrypted_data.txt");

2.3 模拟代码混淆 (手动简单示例)

Auto.js Pro 提供内置的混淆器,这里仅为演示混淆原理。

javascript 复制代码
// 混淆前 (原始代码)
function calculateSum(a, b) {
    let result = a + b;
    return result;
}
log(calculateSum(10, 20));

// 混淆后 (手动模拟,实际混淆器更复杂)
// var _0x123abc = function(_0xdef456, _0x789ghi) {
//     var _0xrstuvw = _0xdef456 + _0x789ghi;
//     return _0xrstuvw;
// };
// log(_0x123abc(10, 20));

// 这种手动混淆主要是为了理解其原理:通过难以阅读的变量名和函数名,增加代码分析难度。
// 实际项目中应使用 Auto.js Pro 自带的混淆功能或其他专业的 JavaScript 混淆工具。

// 另一个混淆思路:字符串加密
var msg = "HelloWorld";
var encryptedMsg = encodeBase64(msg);
// 在脚本运行时再解密使用
var decryptedMsg = decodeBase64(encryptedMsg);
log("混淆字符串解密: " + decryptedMsg);

2.4 敏感信息外部配置与动态加载

避免将敏感信息硬编码在脚本中,可以从外部文件加载。

javascript 复制代码
// config/credentials.json (此文件应加密或安全存储)
// {"username": "myuser", "password": "mypass"}

// main.js
// 假设 credentials.json 已被安全加载并解密
function loadCredentials(filePath, decryptKey) {
    try {
        var encryptedContent = files.read(filePath);
        var decryptedContent = decryptAES(encryptedContent, decryptKey); // 假设已用AES加密
        return JSON.parse(decryptedContent);
    } catch (e) {
        log("加载或解密凭据失败: " + e, "ERROR");
        return null;
    }
}

var CREDENTIALS_FILE = "/sdcard/app_credentials.json";
var APP_KEY = "AnotherSecretKey"; // 用于解密凭据文件的密钥

// 在脚本启动时,先将模拟的加密凭据写入文件
var encryptedCredentials = encryptAES(JSON.stringify({ username: "test_user", password: "test_pwd" }), APP_KEY);
files.write(CREDENTIALS_FILE, encryptedCredentials);

var credentials = loadCredentials(CREDENTIALS_FILE, APP_KEY);
if (credentials) {
    log("用户名: " + credentials.username);
    log("密码: " + credentials.password);
} else {
    log("无法获取用户凭据,脚本无法继续。");
}

2.5 脚本完整性校验 (简单哈希校验)

通过校验脚本文件内容的哈希值,判断脚本是否被篡改。

javascript 复制代码
// 引入必要的 Java 类 (用于 SHA-256 哈希计算)
var MessageDigest = Java.type("java.security.MessageDigest");
var StringBuilder = Java.type("java.lang.StringBuilder");

// 计算文件内容的 SHA-256 哈希值
function calculateFileHash(filePath) {
    try {
        var fis = new java.io.FileInputStream(filePath);
        var digest = MessageDigest.getInstance("SHA-256");
        var buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 1024);
        var bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            digest.update(buffer, 0, bytesRead);
        }
        fis.close();
        var hashBytes = digest.digest();
        var hexString = new StringBuilder();
        for (var i = 0; i < hashBytes.length; i++) {
            var hex = java.lang.Integer.toHexString(0xff & hashBytes[i]);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    } catch (e) {
        log("计算文件哈希失败: " + e, "ERROR");
        return null;
    }
}

var scriptPath = files.path("main.js"); // 假设当前脚本文件是 main.js
var expectedHash = "f32ae8b1d9c0e2a4f6d8b0c1e2f3g4h5i6j7k8l9m0n1o2p3q4r5s6t7u8v9w0x1"; // 假设这是你预期的脚本哈希值

// 实际中,你需要先运行一次脚本,获取它的哈希值作为预期值
var currentHash = calculateFileHash(scriptPath);
log("当前脚本哈希: " + currentHash);

if (currentHash && currentHash === expectedHash) {
    log("脚本完整性校验通过!");
} else {
    log("警告:脚本可能已被篡改或哈希值不匹配!", "WARN");
    // exit(); // 发现篡改可选择退出脚本
}

2.6 防调试与反 Hook 技巧 (高级,仅供了解)

这些技术通常涉及更底层、更复杂的实现,在 Auto.js 环境中可能受限,但了解其原理有助于提升安全意识。

  • 检测调试器 :检查 android.os.Debug.isDebuggerConnected()
  • 代码注入检测:检查关键函数是否被 Hook。
  • 虚拟机检测:判断是否运行在模拟器或虚拟机环境。
javascript 复制代码
// 简单检测调试器 (不保证完全有效)
// var Debug = Java.type("android.os.Debug");
// if (Debug.isDebuggerConnected()) {
//     log("检测到调试器连接,脚本将退出!", "FATAL");
//     exit();
// }

2.7 关键逻辑分发与动态执行

将部分核心逻辑不直接写入主脚本,而是通过网络获取或从加密文件中解密后动态执行。

javascript 复制代码
// main.js
// 假设 getDecryptedCoreLogic() 是一个从网络或加密文件获取并解密核心逻辑的函数
// var coreLogicCode = getDecryptedCoreLogic();
// if (coreLogicCode) {
//     // 动态执行核心逻辑
//     eval(coreLogicCode);
// } else {
//     log("无法加载核心逻辑,脚本停止。", "ERROR");
//     exit();
// }

// 简单示例:一个加密的函数字符串
var encryptedFunc = encryptAES("function doSomethingSecret(){ log('执行了秘密操作!'); }", aesKey);
// ... 在运行时解密并执行 ...
var decryptedFuncCode = decryptAES(encryptedFunc, aesKey);
if(decryptedFuncCode){
    eval(decryptedFuncCode);
    doSomethingSecret(); // 调用解密后的函数
}

三、实战项目:保护核心逻辑的授权验证脚本

3.1 项目需求

开发一个 Auto.js 脚本,其中包含一个重要的核心功能。该核心功能必须经过授权验证才能执行。授权信息通过网络获取并加密存储,每次脚本启动时进行验证。

3.2 项目结构

plaintext 复制代码
autojs-security-demo/
├── main.js                 # 主入口,负责启动和授权验证
├── modules/
│   ├── authManager.js      # 授权管理模块 (验证、获取、存储授权)
│   ├── coreLogic.js        # 加密的核心业务逻辑
│   └── logger.js           # 日志模块
├── data/
│   └── license.dat         # 加密的授权文件
├── logs/
│   └── app.log
└── README.md

3.3 logger.js 日志模块 (复用)

javascript 复制代码
// modules/logger.js
function log(msg, level = "INFO") {
    let line = `[${new Date().toLocaleTimeString()}][${level}] ${msg}`;
    files.append("../logs/app.log", line + "\n");
    console.log(line); // 同时输出到控制台
}

module.exports = { log };

3.4 coreLogic.js (加密的核心逻辑)

这个文件在实际部署时应该是加密的,这里为了演示,直接写明文,但假定它会被 authManager.js 解密后执行。

javascript 复制代码
// modules/coreLogic.js
// 这部分代码通常会被加密或混淆,运行时由授权模块解密

const logger = require('./logger.js');

function executeCoreFunction(param) {
    logger.log("---------------------------------");
    logger.log("核心功能已执行!参数: " + param, "INFO");
    logger.log("执行敏感操作,例如自动化任务...", "INFO");
    logger.log("---------------------------------");
    // 模拟耗时操作
    sleep(2000);
    return "核心功能执行成功!";
}

// 在实际应用中,这里可能不会直接 export,而是将整个函数作为字符串加密
module.exports = {
    executeCoreFunction
};

3.5 authManager.js (授权管理模块)

负责授权的获取、验证、加密存储和核心逻辑的解密执行。

javascript 复制代码
// modules/authManager.js
const logger = require('./logger.js');

// 引入必要的 Java 类 (与上面AES示例相同)
var SecretKeySpec = Java.type("javax.crypto.spec.SecretKeySpec");
var Cipher = Java.type("javax.crypto.Cipher");
var Base64 = Java.type("android.util.Base64");

const LICENSE_FILE = "../data/license.dat";
const ENCRYPTION_KEY = "YourAuthSecretKey!"; // 授权文件加密密钥

// AES 加密函数(与前面示例相同,可复用)
function encryptAES(data, key) {
    try {
        var rawKey = key.getBytes("UTF-8");
        var skeySpec = new SecretKeySpec(rawKey, "AES");
        var cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        var encrypted = cipher.doFinal(data.getBytes("UTF-8"));
        return Base64.encodeToString(encrypted, Base64.NO_WRAP);
    } catch (e) {
        logger.log("AES加密失败: " + e, "ERROR");
        return null;
    }
}

// AES 解密函数(与前面示例相同,可复用)
function decryptAES(encryptedData, key) {
    try {
        var rawKey = key.getBytes("UTF-8");
        var skeySpec = new SecretKeySpec(rawKey, "AES");
        var cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        var original = cipher.doFinal(Base64.decode(encryptedData, Base64.NO_WRAP));
        return new java.lang.String(original, "UTF-8");
    } catch (e) {
        logger.log("AES解密失败: " + e, "ERROR");
        return null;
    }
}

/**
 * 模拟从服务器获取授权信息
 * 实际中这里会进行 HTTP 请求,并可能包含设备指纹、时间戳等校验
 */
function fetchLicenseFromServer(userId) {
    logger.log(`正在从服务器获取用户 ${userId} 的授权...`);
    // 模拟服务器响应:一个 JSON 字符串,包含授权信息和过期时间
    let serverResponse = JSON.stringify({
        user: userId,
        activated: true,
        expiry: Date.now() + (1000 * 60 * 60 * 24 * 3) // 授权三天
    });
    logger.log("服务器返回原始授权信息: " + serverResponse);
    return encryptAES(serverResponse, ENCRYPTION_KEY);
}

/**
 * 验证本地授权文件
 * @returns {boolean} 是否授权成功
 */
function verifyLicense() {
    logger.log("开始验证本地授权...");
    if (!files.exists(LICENSE_FILE)) {
        logger.log("授权文件不存在。", "WARN");
        return false;
    }
    try {
        let encryptedLicense = files.read(LICENSE_FILE);
        let decryptedLicense = decryptAES(encryptedLicense, ENCRYPTION_KEY);
        if (!decryptedLicense) {
            logger.log("授权文件解密失败。", "ERROR");
            return false;
        }
        let licenseInfo = JSON.parse(decryptedLicense);
        if (!licenseInfo.activated || licenseInfo.expiry < Date.now()) {
            logger.log("授权已过期或未激活。", "WARN");
            return false;
        }
        logger.log("授权验证成功!", "INFO");
        return true;
    } catch (e) {
        logger.log("授权文件解析或验证异常: " + e, "ERROR");
        return false;
    }
}

/**
 * 更新授权文件 (通常在获取新授权或续期后调用)
 * @param {string} encryptedLicense 从服务器获取的加密授权字符串
 */
function updateLicense(encryptedLicense) {
    if (!encryptedLicense) {
        logger.log("更新授权失败:无效的加密授权数据。", "ERROR");
        return false;
    }
    try {
        files.write(LICENSE_FILE, encryptedLicense);
        logger.log("授权文件已更新。", "INFO");
        return true;
    } catch (e) {
        logger.log("写入授权文件失败: " + e, "ERROR");
        return false;
    }
}

/**
 * 获取并执行核心逻辑 (模拟动态加载和解密执行)
 * @returns {function|null} 解密后的核心功能函数,如果授权失败则返回 null
 */
function getCoreLogicFunction() {
    if (!verifyLicense()) {
        logger.log("授权验证失败,无法加载核心逻辑。", "FATAL");
        return null;
    }

    logger.log("授权通过,准备加载核心逻辑...");

    // 在实际应用中,coreLogic.js 会被预先加密,这里是模拟解密后eval。
    // 假设 coreLogic.js 的内容被加密为一个字符串
    const originalCoreLogicCode = files.read("../modules/coreLogic.js"); // 假定这个文件是明文的,实际生产环境应是加密的
    // const encryptedCoreLogicCode = "..."; // 实际会是加密的字符串
    // const decryptedCoreLogicCode = decryptAES(encryptedCoreLogicCode, SOME_OTHER_KEY);

    // 为演示,这里直接返回 require 来的函数,跳过 eval 的步骤
    // 实际生产环境:为了保护 coreLogic.js 源码,会对其进行加密或混淆,
    // 然后在这里解密 `eval()` 执行。

    const coreLogic = require('./coreLogic.js');
    return coreLogic.executeCoreFunction;
}

module.exports = {
    verifyLicense,
    fetchLicenseFromServer,
    updateLicense,
    getCoreLogicFunction,
    // 暴露加密解密函数以便 main.js 模拟写入加密文件
    encryptAES,
    decryptAES
};

3.6 main.js 主入口脚本

javascript 复制代码
// main.js
const authManager = require('./modules/authManager.js');
const logger = require('./modules/logger.js');

logger.log("脚本启动:开始授权验证...");

function startApp() {
    // 尝试获取核心功能函数
    let coreFunction = authManager.getCoreLogicFunction();

    if (coreFunction) {
        logger.log("核心功能已准备就绪,开始执行!");
        coreFunction("用户自定义参数"); // 执行核心功能
        logger.log("脚本主流程执行完毕。");
    } else {
        logger.log("授权失败,脚本终止。", "FATAL");
        toast("授权失败,请联系管理员!");
    }
}

// 首次运行或授权过期时,模拟从服务器获取新授权
if (!authManager.verifyLicense()) {
    logger.log("本地授权无效或已过期,尝试从服务器获取新授权...");
    const userId = "user_autojs_001"; // 模拟用户ID
    let encryptedNewLicense = authManager.fetchLicenseFromServer(userId);
    if (encryptedNewLicense) {
        if (authManager.updateLicense(encryptedNewLicense)) {
            logger.log("新授权获取并保存成功,请重新运行脚本以生效。", "INFO");
            toast("授权成功,请重新运行脚本!");
            exit(); // 重新运行脚本以加载新授权
        } else {
            logger.log("保存新授权文件失败。", "ERROR");
            toast("保存授权失败!");
            exit();
        }
    } else {
        logger.log("从服务器获取授权失败。", "ERROR");
        toast("获取授权失败!");
        exit();
    }
} else {
    // 本地授权有效,直接启动应用
    startApp();
}

3.7 运行效果与分析

  • 首次运行 main.js,如果 license.dat 不存在或过期,它会模拟从"服务器"获取新授权,并将其加密保存到 data/license.dat。此时会提示重新运行脚本。
  • 第二次运行 main.js,脚本会读取并验证 license.dat。如果验证成功,将执行 coreLogic.js 中的核心功能。
  • 通过日志,你可以看到授权验证和核心功能执行的整个流程。
  • 这个项目演示了如何将授权验证与核心逻辑分离,并使用加密存储敏感授权信息,增加了脚本的安全性。

四、常见问题与解决方案

问题 解决方案
加密后脚本无法运行/报错 检查加密/解密算法是否匹配,密钥是否正确,编码是否一致。确保 Java 类引用无误。
性能损耗明显 只对核心、敏感代码进行加密/混淆。选择高效的加密算法。考虑运行时解密而非每次启动解密。
密钥管理困难 密钥应从安全渠道获取(如网络 API),而非硬编码。考虑使用 Android Keystore。
脚本被逆向破解 混淆与加密结合使用,增加逆向难度。定期更新加密策略和混淆规则。引入服务端校验。
第三方库/插件兼容性问题 混淆时排除第三方库(通常混淆工具支持)。确保加密的模块能被正确加载。
反调试机制被绕过 反调试只是增加难度,无法完全阻止。结合服务端校验提供更强安全。
敏感信息泄露 严格管理所有敏感数据,避免硬编码。使用加密存储或动态获取。
加密文件读写权限问题 确保脚本拥有存储读写权限 (<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>)
多版本兼容性问题 考虑不同 Auto.js 版本或 Android 系统版本可能对加密库和文件系统行为的影响。
部署与更新复杂 设计合理的模块加载和更新机制,支持远程更新加密模块。

五、性能优化建议

  • 精细化加密范围:只对最核心、最敏感的逻辑进行加密或混淆,非核心功能保持明文,减少运行时解密开销。
  • 高效加密算法:选择性能开销较小的加密算法(如 AES)。避免在性能敏感的循环中进行大量加密解密操作。
  • 缓存解密结果:对于运行时解密的代码或数据,可以考虑解密一次后缓存到内存中,避免重复解密。
  • 异步解密/加载:如果核心逻辑较大,可以考虑在子线程中异步解密和加载,避免阻塞主线程。
  • 服务端验证:将部分授权验证逻辑放到服务器端,每次运行脚本时向服务器请求验证,即使本地脚本被破解,也无法绕过服务器验证。
  • 定期清理:及时清理不再需要的临时解密文件或缓存,减少磁盘占用。
  • 日志分级:在生产环境中,降低日志输出等级,避免打印过多敏感信息或影响性能。
  • 文件操作优化:避免频繁读写加密文件,可以批量读取或写入。
  • 使用 Auto.js Pro 混淆器:Auto.js Pro 自带的混淆功能通常比手动混淆更专业和高效,能有效增加代码逆向难度。
  • 压缩加密数据:在加密前对数据进行压缩,可以减少加密数据的体积,提升传输和存储效率。

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

相关推荐
whysqwhw15 分钟前
Egloo 项目结构分析
android
WAKEUP36916 分钟前
TypeScript 类型系统简述:构建更健壮的代码基础
前端·typescript
難釋懷19 分钟前
Vue-github 用户搜索案例
前端·vue.js
red润21 分钟前
被转义字符麻痹的一天:理解转义字符串
前端·javascript·正则表达式
Wgllss21 分钟前
大型异步下载器(二):基于kotlin+Compose+协程+Flow+Channel+ OKhttp 实现多文件异步同时分片断点续传下载
android·架构·android jetpack
ladymorgana23 分钟前
【 FastJSON 】解析多层嵌套
java·前端·fastjson
晚风30826 分钟前
组件传参方式
前端·vue.js
TE-茶叶蛋33 分钟前
HTML5 更新的功能
前端·html·html5