以下是一篇关于反混淆技术的CSDN博客草稿,使用Markdown格式整理。重点介绍处理组合附加符号和多次注入length方法的垃圾代码自动化方案。
安卓逆向工程:自动化处理混淆代码中的组合附加符号与length注入攻击
背景
在安卓逆向工程中,我们常遇到两类棘手的混淆技术:
- 组合附加符号:Unicode范围[0x0300, 0x036F]的不可见字符
- length方法注入:反复插入无效的字符串length调用
这些技术会干扰反编译工具并增加代码分析难度。本文将介绍一套基于Node.js的自动化处理方案。
整体流程
核心实现步骤
1. 环境准备
# package.json依赖
{
"name": "fankongzhiliuhunxiao",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "node main.js"
},
"dependencies": {
"archiver": "^7.0.1",
"compressing": "^1.10.3",
"unzip-stream": "^0.3.4"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
2. 配置文件 (config.js)
const path = require('path');
const baseConfig = {
// 打印zip日志
zipLog() {
// console.log.apply(console, arguments)
},
// 打印移除无效调用length的日志
callLengthLog() {
// 统计调用长度
// console.log.apply(console, arguments)
},
// 打印连续赋值的代码块中移除无效赋值的日志
removeDefineVariableContinueLog() {
// 打印赋值代码块中移除无效赋值的日志
// console.log.apply(console, arguments)
},
// 输出文件日志
fileLog() {
// console.log.apply(console, arguments)
}
}
const config = {
...baseConfig,
removeEnptyLine: true, // 去除空行
removeDefineVariable: true, // 去除定义的变量,但被检测为无效调用(只被length方法使用且不记录值的情况)
removeDefineVariableContinue: true, // 在一段连续定义字符串的代码块中遍历,对于为同一个变量多次赋值的情况(可以不相邻),只保留最后一次的赋值,【在移除了length调用方法之后执行效果更好,即removeCallLength:true时】
removeCallLength: true, // 移除调用length方法
zipOutName: "classes_smali.zip", // 输出的zip文件名称,注意不是路径,路径和输入的路径都是一样的,自动处理
smaliZipPath: "/storage/emulated/0/qx/smali/classes_smali混淆版2.zip", // 注意改个名字,因为运行输出的文件名是 classes_smali.zip 方便直接用NP转dex
adbDevicesName: "emulator-5554",
inputDir: path.join(__dirname, "ignore/input"),
outputDir: path.join(__dirname, "ignore/output"),
}
// 解除下行注释 当针对文件存在时,会自动复制到根目录,其它都删除
// config.testFile = path.join(config.inputDir, "com/example/vpntest/MainActivity.smali");
config.zipLog = config.zipLog
module.exports = config;
3. 核心处理逻辑 (main.js)
const fs = require('fs');
const path = require('path');
const config = require('./config.js'); // 引入配置文件
let str = `
请用NP管理器:
1.解压出dex文件
2.将dex文件转成smali.zip
3.将smali.zip解压
4.将smali文件夹复制到当前项目的./smali/input 文件夹
5.运行后将 ./smali/output 文件夹内的所有文件压缩(不包括/output)
6.将压缩后的zip文件用NP转成dex即可
====================================
`
console.log(str)
main(config.inputDir, config.outputDir);
/// 业务逻辑
function deletePath(mPath, deep = 0) {
if (fs.existsSync(mPath)) {
const state = fs.statSync(mPath);
if (state.isFile()) {
fs.unlinkSync(mPath);
return;
} else {
const files = fs.readdirSync(mPath);
for (let i = 0; i < files.length; i++) { // 同步
deletePath(path.join(mPath, files[i]), deep + 1);
}
const t = fs.readdirSync(mPath)
fs.rmdirSync(mPath);
}
}
return;
}
// 函数getAllCodePoints用于获取字符串中每个字符的Unicode编码
function getAllCodePoints(str) {
// 使用扩展运算符将字符串转换为字符数组
return [...str].map(char =>
// 使用codePointAt方法获取字符的Unicode编码,并使用toString(16)将其转换为16进制,toUpperCase()将其转换为大写
'U+' + char.codePointAt(0).toString(16).toUpperCase()
);
}
// console.log(getAllCodePoints('x̴̧̨̨͖̤͙͓͎͎̞͖̮̲̦͖̦̞̘͔̩͓̦̭͍̣͉̹̯͔̖̖͐́͂̉'));
// [ 'U+4F60', 'U+597D', 'U+1F60A' ]
function 反混淆字符串(input) {
const 组合附加符号范围 = [0x0300, 0x036F];
const result = [];
[...input].forEach(char => {
// 使用codePointAt方法获取字符的Unicode编码,并使用toString(16)将其转换为16进制,toUpperCase()将其转换为大写
const unicode = char.codePointAt(0);
const 是组合附加符号 = (unicode >= 组合附加符号范围[0] && unicode <= 组合附加符号范围[1]);
if (!是组合附加符号) {
result.push(char);
}
console.log(!是组合附加符号, unicode.toString(16).toUpperCase());
});
return result.join('');
//test console.log(反混淆字符串("x̴̧̨̨͖̤͙͓͎͎̞͖̮̲̦͖̦̞̘͔̩͓̦̭͍̣͉̹̯͔̖̖͐́͂̉"));
}
function 反混淆smali(inputDir, outputDir) {
if (this.rootDir == undefined) {
this.rootDir = inputDir;
}
if (inputDir === outputDir) {
throw new Error('输入目录和输出目录不能相同');
} else if (!fs.existsSync(inputDir)) {
throw new Error('输入目录不存在');
} else if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const 组合附加符号范围 = [0x0300, 0x036F];
const files = fs.readdirSync(inputDir);
files.forEach(file => {
const filePath = path.join(inputDir, file);
const state = fs.statSync(filePath);
if (state.isFile()) {
const inputContent = fs.readFileSync(filePath, 'utf-8');
const tempArray = [];
let resultContent = '';
if (file.endsWith('.smali')) {
[...inputContent].forEach(char => {
// 使用codePointAt方法获取字符的Unicode编码,并使用toString(16)将其转换为16进制,toUpperCase()将其转换为大写
const unicode = char.codePointAt(0);
const 是组合附加符号 = (unicode >= 组合附加符号范围[0] && unicode <= 组合附加符号范围[1]);
if (!是组合附加符号) {
tempArray.push(char);
}
//console.log(!是组合附加符号, unicode.toString(16).toUpperCase());
});
resultContent = 处理无效代码(tempArray.join(''));
} else {
resultContent = inputContent;
}
const savePath = path.join(outputDir, filePath.replace(this.rootDir, ''));
(!fs.existsSync(savePath)) && fs.mkdirSync(path.dirname(savePath), { recursive: true });
fs.writeFileSync(savePath, resultContent);
config.fileLog("保存路径", savePath)
} else if (state.isDirectory()) {
反混淆smali(filePath, outputDir);
}
})
}
function 处理无效代码(code) {
const methodReg = /^(\.method)/; // 匹配 .method 指令
const endMethodReg = /^(\.end method)/; // 匹配 .end method 指令
// const defineStrReg = /const-string\s+(v\d+),/; // 匹配 const-string 指令,并返回字符串
const defineStrReg = /const-string\s+([vp]\d+),/; // 匹配 const-string 指令,并返回字符串
const moveResultReg = /move-result\s/; // 匹配 move-result 指令
let offsetLine = 115; // 下方代码在当前编辑器中的位置
const data = code.split("\n"); // 将字符串按行分割成数组
if (config.removeCallLength) {
let methodStartLine = null;
let methodEndLine = null;
const ignoreLines = []
let 寄存器列表 = null;
let tryCatchDeepth = 0;
const addRemoveLine = (key, reason, obj) => {
if ((key != undefined && key != null) && obj == undefined) {
const temp = 寄存器列表.get(key);
ignoreLines.push({
lineIndex: temp.lineIndex,
value: data[temp.lineIndex],
reason,
key,
deep: temp.deep
})
} else if (obj != undefined && obj != null) {
ignoreLines.push({ ...obj, key })
}
}
for (let i = 0; i < data.length; i++) {
let line = data[i];
if (methodReg.test(line)) {
methodStartLine = i;
寄存器列表 = new Map();
for (; i < data.length && !endMethodReg.test(line); i++) {
line = data[i];
if (line == "") {
continue;
}
if (line.trim().startsWith(":try_start")) {
tryCatchDeepth++;
} else if (line.trim().startsWith(":catch")) {
tryCatchDeepth--;
}
// 找到定义字符串的位置
if (defineStrReg.test(line)) {
// 记录寄存器名称
const key = defineStrReg.exec(line)[1];
if (寄存器列表.get(key) == undefined) {
寄存器列表.set(key, { lineIndex: i, value: line, used: false, deep: tryCatchDeepth });
} else {
if (!寄存器列表.get(key).used && 寄存器列表.get(key).deep == tryCatchDeepth) {
if (config.removeDefineVariable || true) {
const reason = `寄存器被${i + 1}行${line.trim()}重新定义, 且无有效调用`;
addRemoveLine(key, reason)
}
}
寄存器列表.set(key, { lineIndex: i, value: line, used: false, deep: tryCatchDeepth });
}
} else {
// 不是const-string
寄存器列表.forEach(function (value, key) {
const keyReg = new RegExp(`\\b${key}\\b`)
const spaceKeyReg = new RegExp(`\\s\\b${key}\\b`)
const temp = 寄存器列表.get(key)
// 用到了寄存器
// if (data[i].indexOf(key) != -1) {
if (keyReg.test(line)) {
// 是否调用了length方法
// invoke-virtual/range {v17 .. v17}, Ljava/lang/String;->length()I
if (
(data[i].indexOf(`invoke-virtual {${key}}, Ljava/lang/String;->length()I`) != -1)
||
(data[i].indexOf(`invoke-virtual/range {${key} .. ${key}}, Ljava/lang/String;->length()I`) != -1)
) {// 调用length方法
// 检测调用的length方法是否多余
let offset = i + 1;
while (offset < data.length && data[offset].trim() == "") {
offset++;
}
const 非空行 = data[offset];
if (非空行.trim().startsWith("move") == "") {
const reason = (offset + 1) + "行非空->" + 非空行.trim() + "\t不接收返回值的调用length";
addRemoveLine(key, reason, {
lineIndex: i,
value: data[i],
reason,
deep: tryCatchDeepth,
})
} else {
temp.used = true;
}
} else {
temp.used = true;
return;
// 有问题,太复杂,不考虑这些了
// 被覆盖使用的情况
let msg = null;
if (spaceKeyReg.test(line.trim())) {
const position = line.trim().indexOf(key);
const splitChar = line.trim().indexOf(",")
if (line.indexOf("move") != -1) { // move-result move-result-object
if (splitChar != -1 && splitChar < position) { //右边是被读取不是赋值,可忽略
temp.used = true;
} else {
msg = "move";
}
} else if (line.trim().startsWith("const") != "") { // const v0; const/4; const/16
if (splitChar != -1 && splitChar < position) { //右边是被读取不是赋值,可忽略
temp.used = true;
} else {
msg = "const";
}
} else if (line.trim().startsWith("new-") != "" || line.trim().startsWith("field-new-") != "") { // new-instance v0;
if (splitChar != -1 && splitChar < position) { //右边是被读取不是赋值,可忽略
temp.used = true;
} else {
msg = "new-";
}
} else if (line.trim().startsWith("iget") != "") {// iget v0;
if (splitChar != -1 && splitChar < position) { //右边是被读取不是赋值,可忽略
temp.used = true;
} else {
msg = "iget";
}
} else if (line.trim().startsWith("aget") != "") {// iget v0;
if (splitChar != -1 && splitChar < position) { //右边是被读取不是赋值,可忽略
temp.used = true;
} else {
msg = "aget";
}
} else if (line.trim().startsWith("sget") != "") {// iget v0;
if (splitChar != -1 && splitChar < position) { //右边是被读取不是赋值,可忽略
temp.used = true;
} else {
msg = "sget";
}
}
}
if (msg != null) {
// 寄存器被覆盖前的处理
if (temp.used == false && temp.deep == tryCatchDeepth && config.removeDefineVariable) {
const reason = `寄存器被${i + 1}行${line.trim()}覆盖,且无有效调用` + line;
addRemoveLine(key, reason)
}
寄存器列表.delete(key);
}
// 被其他方法使用
else {
temp.used = true;
//console.log("寄存器被使用了", line);
}
}
}
if (endMethodReg.test(line)) {
//console.log("key", key, "方法结束", "used", temp.used, "line", temp.line + 0);
// 方法结束,记录未被有效赋值的寄存器
if (temp.used == false && config.removeDefineVariable) {
//存在定义的字符串没有在当前类中使用的情况,但可能被其他地方使用,所以保留一个定义;
addRemoveLine(key, "方法结束,寄存器赋值 无效调用")
}
}
});
}
}
//console.log(JSON.stringify(ignoreLines, null, 2));
}
}
ignoreLines.sort((a, b) => {
return a.lineIndex - b.lineIndex
})
for (let i = 0; i < ignoreLines.length; i++) {
const lineIndex = ignoreLines[i].lineIndex;
config.callLengthLog("[-]", lineIndex + 1, ignoreLines[i].deep, ignoreLines[i].reason, ignoreLines[i].key, ignoreLines[i].value);
data[lineIndex] = "";
}
// 去重复定义变量,在连续定义字符串的块中 为同一个寄存器多次赋值的,只保留最后一次赋值,包含了间隔的情况
for (let lineIndex = 0; config.removeDefineVariableContinue && lineIndex < data.length; lineIndex++) {
//1. 当前行是定义字符串变量的行
const 变量名列表 = {};
if (data[lineIndex].trim() == "" || !defineStrReg.test(data[lineIndex].trim())) continue; // 非空行不是定义字符串的则跳过
// const 变量 = data[lineIndex].trim().split(",")[0].trim();
const 变量 = defineStrReg.exec(data[lineIndex].trim())[1]
变量名列表[变量] = lineIndex; // 记录此时的行号
lineIndex++;
//2. 遍历连续定义字符串的代码块
while (lineIndex < data.length) {
//2.1. 往下找一个非空行,
if (data[lineIndex].trim() != "") {
// 并且必须是定义字符串的行
if (defineStrReg.test(data[lineIndex].trim())) {
const 后变量 = defineStrReg.exec(data[lineIndex].trim())[1]
if (变量名列表[后变量] != undefined) {
config.removeDefineVariableContinueLog("[-]", 变量名列表[后变量] + 1, lineIndex + 1, "代码块中再次赋值,删除上次", data[变量名列表[后变量]].trim(), "保留", data[lineIndex].trim());
data[变量名列表[后变量]] = "";// 删除为同一变量连续定义字符串的第一行
变量名列表[后变量] = lineIndex
} else {
变量名列表[后变量] = lineIndex
}
}
// 否则就是被截断的其它代码,不再进行处理,退出循环
else {
break;
}
}
lineIndex++;
}
}
}
// console.log("\n" + data.join("\n"));
let result = data.join("\n");
if (config.removeEnptyLine)
while (/\n\n\n/g.test(result)) {
result = result.replace("\n\n\n", "\n\n")
}
return result;
}
function getPathInfo(filePath) {// 不能用系统的,因为这个还要用于处理adb设备上的文件路径
const name = filePath.trim().replace(/[\\]/g, "/").replace(/\/\//g, "/").split("/").pop(); // 文件名
const dir = filePath.replace(name, ""); // 文件夹路径
return { name, dir };
}
async function main(inputDir, outputDir) {
inputDir = path.resolve(inputDir);
outputDir = path.resolve(outputDir);
const zipName = getPathInfo(config.smaliZipPath).name;
const inputZipPath = path.join(__dirname, zipName);
if (!fs.existsSync(inputZipPath)) {
console.log("从adb复制smalizip到当前路径");
await pullFromADBDevice()
}
if (!fs.existsSync(inputDir)) {
if (!fs.existsSync(inputZipPath)) {
throw new Error("输入zip路径不存在");
}
}
// 当针对文件进行处理时,只要这一个文件不为空,就复制到根目录只干他,别的都忽略
let testMode = config.testFile != null && config.testFile != undefined
if (testMode) {
if (fs.existsSync(config.testFile)) {
testMode = true;
console.log("只处理", config.testFile);
let content = fs.readFileSync(config.testFile, "utf-8");
deletePath(inputDir);
fs.mkdirSync(inputDir, { recursive: true });
fs.writeFileSync(path.join(inputDir, path.basename(config.testFile)), content, "utf-8");
content = null; // 释放内存
console.log("针对文件:", config.testFile)
} else {
const name = getPathInfo(config.testFile).name;
const rootFilePath = path.resolve(config.inputDir, name);
if (!fs.existsSync(rootFilePath)) {
console.error("测试文件不存在!", config.testFile, rootFilePath)
throw new Error("测试文件不存在!\n\t", config.testFile, "\n\t", rootFilePath);
} else {
console.log("针对文件:", rootFilePath)
}
}
} else {
// 删除根目录下的所有文件
deletePath(inputDir);
await require("./zip-util.js").unzip(path.join(__dirname, zipName), inputDir)
}
console.log('清空输出目录下的文件', outputDir);
deletePath(outputDir)
反混淆smali(inputDir, outputDir);
if (testMode) {
} else {
const outputZIPDir = path.join(__dirname, config.zipOutName);
await require("./zip-util.js").zip(outputDir, outputZIPDir)
await pushToADBDevice();
}
console.log('\n\n任务完成!');
}
async function pushToADBDevice() {
// 注意路径太长会出问题,所以先进入当前路径再执行方便很多
await executeAdbCommand(`cd ${__dirname} && adb -s ${config.adbDevicesName} push ./${config.zipOutName} ${getPathInfo(config.smaliZipPath).dir}`);
}
async function pullFromADBDevice() {
// 注意路径太长会出问题,所以先进入当前路径再执行方便很多
await executeAdbCommand(`cd ${__dirname} && adb -s ${config.adbDevicesName} pull ${config.smaliZipPath} ./${getPathInfo(config.smaliZipPath).name}`);
}
async function executeAdbCommand(cmd) {
return new Promise((resolve) => {
console.log(cmd);
const exec = require("child_process").exec;
exec(cmd, function (error, stdout, stderr) {
if (error) {
if (stdout) {
if (/No such file or directory/.test(stdout)) {
throw "adb设备中的文件不存在,请检查文件路径是否正确:";
} else if (/device '(.+)' not found/.test(stdout)) {
throw "请先连接设备";
} else {
console.log("stdout", stdout);
}
} else {
console.log("stderr", stderr);
throw error;
}
}
if (stderr == "") {
return resolve(true)
} else {
throw "error"
}
})
})
}
4. 处理压缩文件 (zip-util.js)
const path = require('path');
const fs = require('fs');
const archiver = require('archiver'); // cnpm install archiver
const unzip = require('unzip-stream');
const config = require('./config.js');
function myZip(sourceDir, outputPath) {
return new Promise((resolve, reject) => {
config.zipLog("压缩路径, 不含父目录", sourceDir);
// 配置路径
if (fs.existsSync(path.dirname(outputPath)) == false) {
reject("输出目录不存在");
} else if (fs.existsSync(outputPath)) {
fs.unlinkSync(outputPath);// 删除压缩包文件
}
// 创建输出流
const output = fs.createWriteStream(outputPath);
const archive = archiver('zip', {
zlib: { level: 9 } // 最高压缩级别
});
// 监听事件
output.on('close', () => {
const size = (archive.pointer() / 1024 / 1024).toFixed(2);
config.zipLog(`✅ 压缩完成!压缩包: ${outputPath}`);
config.zipLog(`📦 总大小: ${size} MB`);
resolve(true)
});
archive.on('warning', (err) => {
if (err.code === 'ENOENT') console.warn('⚠️ 文件系统警告:', err);
else reject(err);
});
archive.on('error', (err) => {
reject(err);
});
// 绑定输出流
archive.pipe(output);
// 遍历目录并添加文件
function addFiles(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 递归处理子目录
addFiles(filePath);
} else {
// 添加文件(保留相对路径结构)
const relativePath = path.relative(sourceDir, filePath);
archive.file(filePath, { name: relativePath });
config.zipLog(`➡️ 添加文件: ${relativePath}`);
}
});
}
// 开始压缩
config.zipLog(`🚀 开始压缩目录: ${sourceDir}`);
addFiles(sourceDir);
archive.finalize();
})
}
function myUnzip(zipPath, outputDir) {
// 检查操作系统
return new Promise((resolve, reject) => {
try {
// 验证输入路径
if (!fs.existsSync(zipPath)) {
throw new Error(`ZIP文件不存在: ${zipPath}`);
}
// 创建输出目录(递归创建)
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
config.zipLog(`📦 开始解压: ${zipPath}`);
config.zipLog(`📂 目标路径: ${outputDir}`);
// 创建可读流
const stream = fs.createReadStream(zipPath)
.pipe(unzip.Parse())
.on('entry', (entry) => {
// 安全处理路径
const sanitizedPath = path.join(outputDir, entry.path.replace(/\.\./g, ''));
const dir = path.dirname(sanitizedPath);
// 确保目录存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 处理文件
if (entry.type === 'File') {
entry.pipe(fs.createWriteStream(sanitizedPath));
config.zipLog(`➡️ 解压文件: ${sanitizedPath}`);
} else {
// 处理目录
entry.autodrain();
}
})
.on('close', () => {
config.zipLog('✅ 解压完成');// 等待1秒钟,否则若立即删除文件很可能失败
return setTimeout(() => {
resolve(true);
}, 1000)
})
.on('error', (err) => {
return reject(new Error(`解压失败: ${err.message}`));
});
} catch (err) {
return reject(err);
}
});
}
// zip(__dirname, path.join(__dirname, "classes_smali.zip"))
// myUnzip(path.join(__dirname, "classes_smali混淆版.zip"), path.join(__dirname, "input"))
module.exports = {
zip: myZip,
unzip: myUnzip
};
关键优化技术
组合符号处理
// 原始混淆代码 const-string v0, "m̵̡̫̙͔̜͙͇͈̝̲̱͕̱̤̟̭̭̙̠͎̬̦͇͎͙͓̰̣̻̥̦̒͊̎̈́̾̾̾͒̃͂͐̉̎̌͂̀͒͗͒̇́̀͛̌́͊̓̈́̀́̀̂̀̕̚̚̚͜͝͝͝͝͝͠͝" // 处理后 const-string v0, "m"
length调用优化
# 混淆代码 const-string v2, "s̷̢̧̲̱̳̖̬͙̱͕̮̩̺̣̪̼͔̮͚͓͇͓̲̣̥͇̬̪̹̞̼̣͌͊̄̏̅̇̾͜͠ͅq̸͍̟͍͕̹͉̤̱̏̆͗́̈́̓̈̆̊͝͝͝" invoke-virtual {v2}, Ljava/lang/String;->length()I # 计算v2字符串长度但未使用结果 invoke-virtual {v1}, Ljava/lang/String;->length()I # 计算v1字符串长度 # 优化后 const-string v2, "s̷̢̧̲̱̳̖̬͙̱͕̮̩̺̣̪̼͔̮͚͓͇͓̲̣̥͇̬̪̹̞̼̣͌͊̄̏̅̇̾͜͠ͅq̸͍̟͍͕̹͉̤̱̏̆͗́̈́̓̈̆̊͝͝͝" # 删除了对v2.length()的调用,因为它没有使用结果 invoke-virtual {v1}, Ljava/lang/String;->length()I # 保留这个调用,假设它被使用
使用方式
- usb连接adb (adb devices 查看设备列表)
- np管理器解包,转dex为smali的压缩包
- 配置config.js
- npm install
- npm run build
- smali压缩包转dex, 替换dex改包,可试试安装是否闪退
- 自行源码分析
效果对比
指标 | 处理前 | 处理后 |
---|---|---|
dex文件大小 | 3.78MB | 126.41KB |
smali行数 | 102230 | 28190 |
可读性评分 | ★☆☆☆☆ | ★★★★☆ |
注意事项
- 正则表达式匹配时必须考虑边界情况(已考虑)
- 操作前备份原始文件
- 针对不同设备调整ADB坐标操作
完整工具链
├── main.js # 核心处理逻辑
├── config.js # 配置文件
└── zip-util.js # 压缩包处理
实测截图
smali对比
转java格式对比
这篇博客详细介绍了处理简单混淆代码的技术方案,包含可运行的代码示例和技术原理说明。