前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
第十五章:脚本加密与安全防护
一、理论讲解:脚本加密、混淆与安全防护的意义
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 自带的混淆功能通常比手动混淆更专业和高效,能有效增加代码逆向难度。
- 压缩加密数据:在加密前对数据进行压缩,可以减少加密数据的体积,提升传输和存储效率。
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!