dex反混淆的思路与实现

发布于:2025-07-09 ⋅ 阅读:(18) ⋅ 点赞:(0)

以下是一篇关于反混淆技术的CSDN博客草稿,使用Markdown格式整理。重点介绍处理组合附加符号和多次注入length方法的垃圾代码自动化方案。


安卓逆向工程:自动化处理混淆代码中的组合附加符号与length注入攻击

背景

在安卓逆向工程中,我们常遇到两类棘手的混淆技术:

  1. 组合附加符号:Unicode范围[0x0300, 0x036F]的不可见字符
  2. length方法注入:反复插入无效的字符串length调用

这些技术会干扰反编译工具并增加代码分析难度。本文将介绍一套基于Node.js的自动化处理方案。

整体流程

原始APK
NP管理器转smali.zip
ADB传输到PC
Node.js处理
去除组合附加符号
优化length调用
清理无效赋值
处理后的smali
重新打包zip
ADB传回设备
NP管理器转dex重打包

核心实现步骤

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
};

关键优化技术

  1. 组合符号处理

    // 原始混淆代码
    const-string v0, "m̵̡̫̙͔̜͙͇͈̝̲̱͕̱̤̟̭̭̙̠͎̬̦͇͎͙͓̰̣̻̥̦̒͊̎̈́̾̾̾͒̃͂͐̉̎̌͂̀͒͗͒̇́̀͛̌́͊̓̈́̀́̀̂̀̕̚̚̚͜͝͝͝͝͝͠͝"
    
    
    // 处理后
    const-string v0, "m"
    
  2. 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  # 保留这个调用,假设它被使用
    

使用方式

  1. usb连接adb (adb devices 查看设备列表)
  2. np管理器解包,转dex为smali的压缩包
  3. 配置config.js
  4. npm install
  5. npm run build
  6. smali压缩包转dex, 替换dex改包,可试试安装是否闪退
  7. 自行源码分析

效果对比

指标 处理前 处理后
dex文件大小 3.78MB 126.41KB
smali行数 102230 28190
可读性评分 ★☆☆☆☆ ★★★★☆

注意事项

  1. 正则表达式匹配时必须考虑边界情况(已考虑)
  2. 操作前备份原始文件
  3. 针对不同设备调整ADB坐标操作

完整工具链

├── main.js    		# 核心处理逻辑
├── config.js       # 配置文件
└── zip-util.js		# 压缩包处理

实测截图

smali对比
在这里插入图片描述
转java格式对比
在这里插入图片描述


这篇博客详细介绍了处理简单混淆代码的技术方案,包含可运行的代码示例和技术原理说明。


网站公告

今日签到

点亮在社区的每一天
去签到