Skip to content

Latest commit

 

History

History
520 lines (429 loc) · 15.1 KB

File metadata and controls

520 lines (429 loc) · 15.1 KB

你提到的是在Android 8以下系统中使用Frida配合fdex2进行应用脱壳的工具组合。让我详细介绍一下:

核心工具说明

1. Frida

  • 动态插桩工具,通过JavaScript代码注入到目标进程
  • 可Hook Java方法和Native函数
  • 支持Android 4.4-11+(不同版本兼容性不同)

2. fdex2

  • 专门用于Android应用的DEX文件脱壳工具
  • 基于Xposed框架或Frida实现
  • 主要针对市面上常见的加固方案(如梆梆、360、腾讯等)

Android 8以下环境配置

准备工作

# 安装Frida
pip install frida-tools

# 下载对应架构的frida-server
# 查看设备架构:adb shell getprop ro.product.cpu.abi

fdex2的Frida版本使用

方法一:直接使用fdex2的Frida脚本

// fdex2_frida.js 核心代码示例
Java.perform(function() {
    var classLoader = Java.use("java.lang.ClassLoader");
    classLoader.loadClass.overload('java.lang.String').implementation = function(name) {
        var result = this.loadClass(name);
        var DexFile = Java.use("dalvik.system.DexFile");
        // 获取DEX文件数据并导出
        // ...
        return result;
    };
});

方法二:改进版的多DEX支持

Java.perform(function() {
    var DexFile = Java.use("dalvik.system.DexFile");
    var mCookie = DexFile.mCookie.value;
    
    // 遍历所有已加载的DEX
    var dexFiles = DexFile.getDexFileList(mCookie);
    for (var i = 0; i < dexFiles.length; i++) {
        var dexPath = dexFiles[i];
        dumpDexFile(dexPath);
    }
});

具体操作步骤

1. 环境搭建

# 推送frida-server到设备
adb push frida-server-android /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server-android
adb shell /data/local/tmp/frida-server-android &

2. 使用fdex2脱壳

# 查找目标进程
frida-ps -U

# 附加到目标应用
frida -U -f com.target.app -l fdex2.js --no-pause

3. 自动脱壳脚本示例

// auto_dump.js
setImmediate(function() {
    Java.perform(function() {
        var Application = Java.use("android.app.Application");
        Application.attach.overload('android.content.Context').implementation = function(context) {
            var result = this.attach(context);
            // 在应用启动时执行脱壳
            dumpAllDexFiles();
            return result;
        };
    });
});

针对不同加固的Hook点

通用Hook方案

// Hook DexClassLoader
var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
DexClassLoader.loadClass.overload('java.lang.String').implementation = function(name) {
    console.log("[*] Loading class: " + name);
    dumpCurrentDex();
    return this.loadClass(name);
};

// Hook BaseDexClassLoader
var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
BaseDexClassLoader.findClass.overload('java.lang.String').implementation = function(name) {
    var result = this.findClass(name);
    var pathList = this.pathList.value;
    // 通过pathList获取DEX文件
    return result;
};

注意事项

  1. Android版本兼容性

    • Android 5.0-7.1 支持较好
    • 部分Android 4.4设备可能需要调整
  2. 加固对抗

    • 某些加固会检测Frida
    • 可能需要反反调试技巧
  3. 权限要求

    adb root  # 需要root权限
    adb remount
  4. 多DEX处理

    • 现代应用可能有多个DEX文件
    • 需要遍历所有已加载的DEX

进阶技巧

绕过检测

// 隐藏Frida特征
Java.perform(function() {
    var System = Java.use("java.lang.System");
    System.getProperty.overload('java.lang.String').implementation = function(key) {
        if (key.contains("frida")) return null;
        return this.getProperty(key);
    };
});

内存DEX搜索

// 搜索内存中的DEX文件头(magic: dex\n035\0)
Memory.scan(ptr(0), Process.enumerateRanges('rw-')[0].size, 
    "6465780a30333500", {
    onMatch: function(address, size) {
        console.log("[+] Found DEX at: " + address);
        dumpMemoryToFile(address, size);
    }
});

替代工具推荐

  1. DumpDex - 更新更活跃的脱壳工具
  2. Youpk - 针对抽取壳的脱壳工具
  3. FDex2(Xposed版) - 更稳定的Xposed实现

总结

对于Android 8以下系统,Frida + fdex2组合是比较成熟的脱壳方案,但需要注意:

  • 确保设备已root
  • 选择合适版本的frida-server
  • 根据目标应用的加固类型调整Hook点
  • 及时保存脱出的DEX文件

需要针对特定应用分析时,可以先尝试通用脚本,再根据日志调整具体的Hook策略。

使用mCookie进行DEX脱壳是一种经典的基于Frida的动态脱壳方法。让我详细解释其原理和实现方式:

原理说明

在Android的Dalvik虚拟机中:

  • mCookie 是DexFile对象的内部标识符,指向已加载的DEX文件内存数据
  • 每个DEX文件加载后都有一个对应的cookie值
  • 通过cookie可以获取DEX文件的原始字节数据

核心代码实现

基础版本:通过ClassLoader获取mCookie

Java.perform(function() {
    // 方法1:Hook PathClassLoader
    var PathClassLoader = Java.use("dalvik.system.PathClassLoader");
    PathClassLoader.loadClass.overload('java.lang.String').implementation = function(name) {
        console.log("[*] Loading class: " + name);
        
        // 获取父类的pathList
        var pathList = this.pathList.value;
        if (pathList) {
            // 获取dexElements数组
            var dexElements = pathList.dexElements.value;
            for (var i = 0; i < dexElements.length; i++) {
                var dexFile = dexElements[i].dexFile.value;
                var cookie = dexFile.mCookie.value;
                dumpDexByCookie(cookie, "dex_" + i + ".dex");
            }
        }
        
        return this.loadClass(name);
    };
});

方法2:直接枚举所有已加载的DEX

function dumpAllDexByCookie() {
    Java.perform(function() {
        // 获取当前应用的ClassLoader
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var classLoader = currentApplication.getClassLoader();
        
        // 获取BaseDexClassLoader的pathList
        var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
        var pathList = classLoader.pathList.value;
        
        if (pathList) {
            console.log("[*] Found pathList");
            var dexElements = pathList.dexElements.value;
            console.log("[*] Number of dexElements: " + dexElements.length);
            
            for (var i = 0; i < dexElements.length; i++) {
                try {
                    var element = dexElements[i];
                    if (element) {
                        var dexFile = element.dexFile.value;
                        if (dexFile) {
                            var cookie = dexFile.mCookie.value;
                            console.log("[*] DEX " + i + " cookie: " + cookie);
                            
                            if (cookie) {
                                // Android 5.0+ mCookie是long类型,需要处理
                                dumpDexFromCookie(cookie, "classes" + (i > 0 ? i : "") + ".dex");
                            }
                        }
                    }
                } catch (e) {
                    console.log("[!] Error processing element " + i + ": " + e);
                }
            }
        }
    });
}

关键:cookie转原始DEX数据

function dumpDexFromCookie(cookie, filename) {
    // cookie在不同Android版本中的处理
    var cookiePtr = null;
    
    if (typeof cookie === 'number' || typeof cookie === 'long') {
        // Android 5.0+:mCookie是指向指针数组的long值
        var cookieArray = ptr(cookie);
        var index = 0;
        
        while (true) {
            var dexPtr = cookieArray.add(index * Process.pointerSize).readPointer();
            if (dexPtr.isNull()) {
                break;
            }
            
            // 检查DEX魔数
            var magic = dexPtr.readCString(8);
            if (magic && magic.includes("dex")) {
                console.log("[+] Found DEX at: " + dexPtr + ", magic: " + magic);
                dumpMemoryToFile(dexPtr, getDexSize(dexPtr), filename + "_part" + index + ".dex");
            }
            
            index++;
        }
    } else if (cookie instanceof NativePointer) {
        // 直接是指针的情况
        dumpSingleDex(cookie, filename);
    }
}

function dumpSingleDex(dexPtr, filename) {
    // 读取DEX文件头
    var dexSize = getDexSize(dexPtr);
    
    if (dexSize > 0) {
        console.log("[+] Dumping DEX: " + filename + ", size: " + dexSize + " bytes");
        
        // 读取内存数据
        var dexData = dexPtr.readByteArray(dexSize);
        
        // 写入文件
        var file = new File("/sdcard/" + filename, "wb");
        file.write(dexData);
        file.close();
        
        console.log("[√] Dumped to /sdcard/" + filename);
    }
}

function getDexSize(dexPtr) {
    try {
        // 从DEX文件头获取文件大小(偏移0x20)
        var size = dexPtr.add(0x20).readU32();
        
        // 验证是否为合理的DEX大小(1KB - 100MB)
        if (size > 1024 && size < 100 * 1024 * 1024) {
            return size;
        }
    } catch (e) {
        console.log("[!] Error reading DEX size: " + e);
    }
    
    // 备用方法:扫描查找结束标志
    return findDexEnd(dexPtr);
}

function findDexEnd(startPtr) {
    var current = startPtr;
    var maxSize = 50 * 1024 * 1024; // 最多50MB
    
    // 尝试查找DEX结束特征或下一个DEX的开始
    for (var offset = 0x70; offset < maxSize; offset += 4) {
        var checkPtr = startPtr.add(offset);
        try {
            var magic = checkPtr.readCString(8);
            if (magic && (magic.includes("dex") || magic.includes("odex"))) {
                // 找到下一个DEX文件头,当前位置就是当前DEX的结束
                return offset;
            }
        } catch (e) {
            break;
        }
    }
    
    return maxSize; // 返回最大尝试大小
}

完整自动化脚本

// mCookie_dumper.js
var dumpedFiles = new Set();

function dumpViaCookie() {
    Java.perform(function() {
        // Hook关键点:当DEX文件被打开时
        var DexFile = Java.use("dalvik.system.DexFile");
        
        // 方法1:Hook loadDex
        DexFile.loadDex.overload('java.lang.String', 'java.lang.String', 'int').implementation = function(sourcePath, outputPath, flags) {
            console.log("[*] loadDex called: " + sourcePath);
            var result = this.loadDex(sourcePath, outputPath, flags);
            dumpDexFromObject(result);
            return result;
        };
        
        // 方法2:Hook DexFile构造函数
        DexFile.$init.overload('java.lang.String', 'java.lang.String', 'int').implementation = function(sourceName, outputName, flags) {
            console.log("[*] DexFile init: " + sourceName);
            var result = this.$init(sourceName, outputName, flags);
            dumpDexFromObject(this);
            return result;
        };
        
        // 方法3:通过ClassLoader枚举(更彻底)
        setTimeout(function() {
            enumerateAllClassLoaders();
        }, 3000);
    });
}

function dumpDexFromObject(dexFileObj) {
    try {
        var cookie = dexFileObj.mCookie.value;
        if (cookie) {
            var timestamp = new Date().getTime();
            var filename = "dex_cookie_" + timestamp + "_" + dumpedFiles.size + ".dex";
            
            if (!dumpedFiles.has(cookie.toString())) {
                dumpedFiles.add(cookie.toString());
                dumpDexFromCookie(cookie, filename);
            }
        }
    } catch (e) {
        console.log("[!] Error dumping from object: " + e);
    }
}

function enumerateAllClassLoaders() {
    Java.enumerateClassLoaders({
        onMatch: function(loader) {
            console.log("[*] Enumerating class loader: " + loader);
            try {
                if (loader && loader.pathList) {
                    var dexElements = loader.pathList.dexElements.value;
                    if (dexElements) {
                        for (var i = 0; i < dexElements.length; i++) {
                            var dexFile = dexElements[i].dexFile.value;
                            if (dexFile) {
                                dumpDexFromObject(dexFile);
                            }
                        }
                    }
                }
            } catch (e) {
                console.log("[!] Error enumerating: " + e);
            }
        },
        onComplete: function() {
            console.log("[√] Class loader enumeration complete");
        }
    });
}

// 启动脱壳
setImmediate(dumpViaCookie);

使用方式

# 1. 启动frida-server
adb shell /data/local/tmp/frida-server &

# 2. 附加到目标进程
frida -U -f com.target.app -l mCookie_dumper.js --no-pause

# 或附加到已运行的进程
frida -U com.target.app -l mCookie_dumper.js

处理不同Android版本

Android 5.0以下(Dalvik)

function dumpDalvikCookie(cookie) {
    // Dalvik中cookie直接是DEX的内存地址
    var dexPtr = ptr(cookie);
    dumpSingleDex(dexPtr, "dalvik.dex");
}

Android 5.0+(ART)

function dumpARTCookie(cookie) {
    // ART中cookie是指向DexFile指针数组的指针
    var cookieArray = ptr(cookie);
    var index = 0;
    
    while (true) {
        var dexFilePtr = cookieArray.add(index * Process.pointerSize).readPointer();
        if (dexFilePtr.isNull()) break;
        
        // ART DexFile结构更复杂,需要读取特定偏移
        var beginPtr = dexFilePtr.add(Process.pointerSize * 2).readPointer();
        var size = dexFilePtr.add(Process.pointerSize * 3).readU32();
        
        if (size > 0) {
            dumpMemoryToFile(beginPtr, size, "art_" + index + ".dex");
        }
        index++;
    }
}

优化技巧

  1. 去重机制
var dumpedCookies = new Map();

function shouldDump(cookie) {
    var key = cookie.toString();
    if (!dumpedCookies.has(key)) {
        dumpedCookies.set(key, true);
        return true;
    }
    return false;
}
  1. 批量导出
function batchDump(dexFiles) {
    dexFiles.forEach(function(dexFile, index) {
        setTimeout(function() {
            dumpDexFromObject(dexFile);
        }, index * 100); // 避免同时大量IO操作
    });
}
  1. 实时监控
// 监控新DEX加载
Java.scheduleOnMainThread(function() {
    setInterval(function() {
        checkNewDexFiles();
    }, 5000);
});

注意事项

  1. 权限问题:确保有sd卡写入权限
  2. 内存访问:访问无效指针可能导致崩溃
  3. 加固对抗:某些加固会清空mCookie或伪装
  4. 性能影响:大量DEX文件时注意性能

这种方法的优势是直接访问虚拟机内部数据结构,可以脱出内存中的原始DEX,包括动态加载的DEX文件。