GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- frida脚本 之 dump 自动化动脱 so

为什么我要动态在内存中查找so并下载修复一个so,因为这个so文件被安全软件进行了加固处理使得代码大面积加密,用ida打开后会发现代码是红色的报错

用到的脱壳so的工具:

https://github.com/lasting-yang/frida_dump/tree/master/android 

原版不好用,存在的问题有不支持远程连接,不支持延迟加载so导致有些so文件无法获取。我进行了脚本优化,支持多个adb设备指定。

前提是已经有一个root手机,安装了magisk,并安装好了frida,并且app没有检测root和frida。

 


整体功能:这是一个用于从Android应用中dump(提取)SO库文件的工具

主要流程:

设备连接:自动检测并选择连接的Android设备
进程附加:使用Frida连接到目标应用(得物App)
模块查找:在应用的内存中查找指定的SO库
内存dump:将SO库从内存中提取出来
文件修复:使用SoFixer工具修复dump出来的文件,让它能正常使用
清理工作:删除临时文件
关键技术点:


使用Frida进行动态分析和内存操作
通过ADB与Android设备通信
处理ARM32和ARM64两种架构
包含重试机制确保模块加载完成



1. 动态内存分析原理 Frida Hook技术: Frida是一个动态插桩框架,可以在运行时注入JavaScript代码到目标进程 通过Hook系统调用和内存操作,获取进程的内存布局信息 不需要root权限就能访问应用的内存空间 内存映射获取: # 通过Frida脚本获取所有已加载的模块信息 allmodule = script.exports_sync.allmodule() # 每个模块包含:名称、基址、大小、路径等信息 2. SO文件在内存中的存储原理 ELF文件加载过程: Android系统加载SO文件时,会将其映射到进程的虚拟内存空间 系统会记录每个SO文件的基址(base address)和大小 文件在内存中可能被分散存储,但逻辑上是连续的 内存布局: 进程内存空间: [0x7000000000] - libc.so [0x7001000000] - libssl.so [0x7002000000] - 目标SO文件 ← 我们要提取的 [0x7003000000] - 其他库文件 3. 内存Dump的核心机制 直接内存读取: // 在dump_so.js中(Frida脚本) function dumpmodule(module_name) { var module = Process.findModuleByName(module_name); if (module) { // 直接从内存基址读取指定大小的数据 var buffer = Memory.readByteArray(module.base, module.size); return buffer; } } 关键步骤: 通过Process.findModuleByName()定位模块 获取模块的基址和大小信息 使用Memory.readByteArray()直接读取内存数据 将二进制数据传回Python端保存为文件 4. 文件修复的必要性 为什么需要修复: 从内存dump出来的SO文件可能缺少某些段信息 内存中的地址是虚拟地址,需要转换为文件偏移 某些重定位信息可能已被修改 SoFixer工具原理: # SoFixer的作用 /data/local/tmp/SoFixer -m 基址 -s 源文件 -o 输出文件 -m:指定原始内存基址,用于地址重定位 重建ELF文件头和段表 修复符号表和重定位表 确保文件可以被IDA、Ghidra等工具正确解析

 

脚本如下:


# 导入系统相关模块,用于处理命令行参数和系统操作
import sys
# 导入frida模块,这是一个动态分析工具,用于hook和调试应用程序
import frida
# 导入操作系统模块,用于执行系统命令
import os
# 导入时间模块,用于延时等操作
import time
# 导入命令行参数解析模块
import argparse
# 导入subprocess模块,用于更好的进程控制和编码处理
import subprocess

# 安全执行系统命令,处理编码问题
def safe_system_call(cmd):
    try:
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding='utf-8', errors='ignore')
        return result.returncode == 0
    except Exception as e:
        print(f"命令执行失败: {cmd}, 错误: {e}")
        return False

# 定义修复SO文件的函数,用于修复从内存中dump出来的SO文件
def fix_so(arch, origin_so_name, so_name, base, size, device_id=None):
    # 设置adb命令,如果没有指定设备ID就用默认的adb
    adb_cmd = "adb"
    # 如果指定了设备ID,就在adb命令中加上设备选择参数
    if device_id:
        adb_cmd = f"adb -s {device_id}"
   
    # 根据设备架构选择对应的SoFixer工具
    # 如果是32位ARM架构,推送32位的修复工具
    if arch == "arm":
        safe_system_call(f"{adb_cmd} push android/SoFixer32 /data/local/tmp/SoFixer")
    # 如果是64位ARM架构,推送64位的修复工具
    elif arch == "arm64":
        safe_system_call(f"{adb_cmd} push android/SoFixer64 /data/local/tmp/SoFixer")
   
    # 给SoFixer工具添加执行权限
    safe_system_call(f"{adb_cmd} shell chmod +x /data/local/tmp/SoFixer")
    # 将需要修复的SO文件推送到手机的临时目录
    safe_system_call(f"{adb_cmd} push {so_name} /data/local/tmp/{so_name}")
   
    # 构建修复命令:-m指定内存基址,-s指定源文件,-o指定输出文件
    fix_cmd = f"{adb_cmd} shell /data/local/tmp/SoFixer -m {base} -s /data/local/tmp/{so_name} -o /data/local/tmp/{so_name}.fix.so"
    # 打印修复命令,方便调试
    print(fix_cmd)
    # 执行修复命令
    safe_system_call(fix_cmd)
   
    # 生成输出文件名,包含原始名称、基址和大小信息
    output_file = f"{origin_so_name}_{base}_{str(size)}_fix.so"
    # 将修复后的文件从手机拉取到电脑
    safe_system_call(f"{adb_cmd} pull /data/local/tmp/{so_name}.fix.so {output_file}")
    # 清理手机上的临时文件:原始dump文件
    safe_system_call(f"{adb_cmd} shell rm /data/local/tmp/{so_name}")
    # 清理手机上的临时文件:修复后的文件
    safe_system_call(f"{adb_cmd} shell rm /data/local/tmp/{so_name}.fix.so")
    # 清理手机上的临时文件:修复工具
    safe_system_call(f"{adb_cmd} shell rm /data/local/tmp/SoFixer")

    # 返回修复后的文件名
    return output_file

# 读取Frida JavaScript脚本文件的内容
def read_frida_js_source():
    # 打开dump_so.js文件并读取全部内容,使用UTF-8编码
    with open("dump_so.js", "r", encoding='utf-8') as f:
        return f.read()

# Frida消息回调函数,当JavaScript脚本发送消息时会调用这个函数
def on_message(message, data):
    # 这里暂时不处理任何消息,只是一个空函数
    pass

# 获取连接的Android设备ID
def get_device_id(specified_device=None):
    # 如果指定了设备ID,直接返回
    if specified_device:
        return specified_device
       
    # 执行adb devices命令获取连接的设备列表,使用UTF-8编码处理输出
    try:
        result = os.popen("adb devices").read()
        devices = result.splitlines()
    except UnicodeDecodeError:
        # 如果UTF-8解码失败,尝试使用系统默认编码
        import subprocess
        result = subprocess.check_output("adb devices", shell=True, encoding='utf-8', errors='ignore')
        devices = result.splitlines()
   
    # 创建空列表存储设备ID
    device_list = []
   
    # 遍历设备列表(跳过第一行标题)
    for line in devices[1:]:
        # 如果这一行不为空且包含"device"字样,说明是一个有效设备
        if line.strip() and "device" in line:
            # 提取设备ID(用制表符分割,取第一部分)
            device_list.append(line.split('\t')[0])
   
    # 如果没有连接任何设备
    if len(device_list) == 0:
        print("No devices connected")
        return None
    # 如果只连接了一个设备,直接返回这个设备ID
    elif len(device_list) == 1:
        return device_list[0]
    # 如果连接了多个设备,让用户选择
    else:
        print("Multiple devices found:")
        # 显示所有设备供用户选择
        for i, dev in enumerate(device_list, 1):
            print(f"{i}. {dev}")
        # 让用户输入选择
        choice = input("Select device (1-{}): ".format(len(device_list)))
        try:
            # 返回用户选择的设备(索引从0开始,所以要减1)
            return device_list[int(choice)-1]
        except:
            # 如果输入无效,使用第一个设备
            print("Invalid selection, using first device")
            return device_list[0]

# 解析命令行参数
def parse_arguments():
    parser = argparse.ArgumentParser(
        description='Android SO文件内存dump和修复工具',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
使用示例:
  # 列出所有已加载的模块
  python dump_so.py -p com.example.app
 
  # dump指定的SO文件
  python dump_so.py -p com.example.app -s libnative.so
 
  # 指定设备ID
  python dump_so.py -d emulator-5554 -p com.example.app -s libnative.so
 
  # 完整命令
  python dump_so.py --device emulator-5554 --package com.example.app --so libnative.so
        ''')
   
    parser.add_argument('-d', '--device',
                       help='指定Android设备ID (可通过adb devices查看)')
   
    parser.add_argument('-p', '--package',
                       required=True,
                       help='目标应用包名 (必需参数)')
   
    parser.add_argument('-s', '--so',
                       help='要dump的SO文件名 (不指定则列出所有模块)')
   
    parser.add_argument('--timeout',
                       type=int,
                       default=10,
                       help='查找模块的超时时间(秒) (默认: 10)')
   
    parser.add_argument('--no-fix',
                       action='store_true',
                       help='只dump不修复SO文件')
   
    return parser.parse_args()

# 验证设备连接
def validate_device(device_id):
    if not device_id:
        return False
       
    # 检查设备是否真的存在,使用安全的编码处理
    try:
        devices = os.popen("adb devices").read()
    except UnicodeDecodeError:
        import subprocess
        devices = subprocess.check_output("adb devices", shell=True, encoding='utf-8', errors='ignore')
   
    if device_id not in devices:
        print(f"错误: 设备 {device_id} 未连接")
        print("可用设备:")
        safe_system_call("adb devices")
        return False
    return True

# 主要的dump逻辑函数
def dump_so_module(script, so_name, device_id, no_fix=False, timeout=10):
    print(f"Looking for module: {so_name}")
   
    # 添加重试逻辑,因为模块可能还没有加载完成
    module_info = None
    # 最多重试指定次数查找模块
    for attempt in range(timeout):
        # 调用JavaScript脚本的findmodule函数查找指定模块
        module_info = script.exports_sync.findmodule(so_name)
        if module_info:
            break
        print(f"Module not found, retry {attempt+1}/{timeout}...")
        # 等待1秒后重试
        time.sleep(1)
   
    # 如果最终还是没找到模块,显示错误信息并退出
    if not module_info:
        print(f"错误: 找不到模块 {so_name}")
        print("当前已加载的模块:")
        # 显示当前所有已加载的模块供参考
        for m in script.exports_sync.allmodule():
            print(f"  {m['name']}")
        return False
   
    # 找到模块后,显示模块信息
    print("找到模块信息:")
    print(f"  名称: {module_info['name']}")
    print(f"  基址: {module_info['base']}")
    print(f"  大小: {module_info['size']}")
    print(f"  路径: {module_info.get('path', 'N/A')}")
   
    # 获取模块的内存基址和大小
    base = module_info["base"]
    size = module_info["size"]
    print(f"开始dump模块: base={base}, size={size}")
   
    # 调用JavaScript脚本的dumpmodule函数dump模块内容
    module_buffer = script.exports_sync.dumpmodule(so_name)
    # 如果dump成功(返回值不是-1)
    if module_buffer != -1:
        # 生成dump文件名
        dump_so_name = so_name + ".dump.so"
        # 将dump的二进制数据写入文件
        with open(dump_so_name, "wb") as f:
            f.write(module_buffer)
            print(f"保存dump文件: {dump_so_name}")
       
        # 如果不需要修复,直接返回
        if no_fix:
            print("跳过修复步骤")
            return True
           
        # 获取设备架构信息
        arch = script.exports_sync.arch()
        print(f"设备架构: {arch}")
       
        # 调用修复函数修复dump出来的SO文件
        try:
            fix_so_name = fix_so(arch, so_name, dump_so_name, base, size, device_id)
            print(f"修复后的SO文件: {fix_so_name}")
            # 删除临时的dump文件
            os.remove(dump_so_name)
            print("清理临时文件完成")
            return True
        except Exception as e:
            print(f"修复过程出错: {e}")
            print(f"原始dump文件保留: {dump_so_name}")
            return False
    else:
        print("dump失败")
        return False

# 程序主入口,只有直接运行这个脚本时才会执行
def main():
    # 解析命令行参数
    args = parse_arguments()
   
    print("=" * 50)
    print("Android SO文件内存dump工具")
    print("=" * 50)
   
    # 获取要使用的Android设备ID
    device_id = get_device_id(args.device)
    # 验证设备连接
    if not validate_device(device_id):
        sys.exit(1)
   
    print(f"使用设备: {device_id}")
    print(f"目标包名: {args.package}")
   
    try:
        # 连接到USB设备(通过Frida)
        device = frida.get_usb_device()
       
        # 尝试附加到正在运行的进程
        try:
            session = device.attach(args.package)
            print(f"已附加到运行中的进程: {args.package}")
        except frida.ProcessNotFoundError:
            print(f"进程未运行,正在启动: {args.package}")
            # 如果进程没有运行,就启动它
            pid = device.spawn([args.package])
            # 附加到新启动的进程
            session = device.attach(pid)
            # 恢复进程执行
            device.resume(pid)
            print(f"已启动并附加到进程: {args.package} (PID: {pid})")
       
        # 创建Frida脚本,加载JavaScript代码
        script = session.create_script(read_frida_js_source())
        # 设置消息回调函数
        script.on('message', on_message)
        # 加载并执行脚本
        script.load()
        print("Frida脚本已加载")
       
        # 等待一下让应用完全启动
        print("等待应用完全启动...")
        time.sleep(3)

        # 如果没有指定SO文件名,就列出所有已加载的模块
        if not args.so:
            print("\n已加载的模块:")
            print("-" * 40)
            # 调用JavaScript脚本的allmodule函数获取所有已加载的模块
            allmodule = script.exports_sync.allmodule()
            # 遍历并打印每个模块的名称
            for i, module in enumerate(allmodule, 1):
                print(f"{i:3d}. {module['name']}")
                if 'path' in module:
                    print(f"    路径: {module['path']}")
            print(f"\n总共找到 {len(allmodule)} 个模块")
        else:
            # dump指定的SO文件
            print(f"\n开始dump SO文件: {args.so}")
            print("-" * 40)
            success = dump_so_module(script, args.so, device_id, args.no_fix, args.timeout)
            if success:
                print("\n✓ dump完成!")
            else:
                print("\n✗ dump失败!")
                sys.exit(1)
               
    except frida.ServerNotRunningError:
        print("错误: Frida服务未运行,请确保设备已root并安装frida-server")
        sys.exit(1)
    except frida.ProcessNotFoundError:
        print(f"错误: 找不到进程 {args.package}")
        print("请确保包名正确且应用已安装")
        sys.exit(1)
    except Exception as e:
        print(f"发生错误: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

 

我希望用aya工具箱进行远程连接手机,手机打开远程调试功能,输入ip和端口就可以远程adb了。

注意要将adb命令配置为环境变量,这样这个脚本就可以进行使用adb进行远程拉取脱壳后的so了。我们会发现目录下多了一个修复好的so文件。

注意过掉frida检测。我这个app过检测的方法是在/data/app的/lib下删除libmasaosec.so这个验证文件,如果不删除在执行脚本时会发现应用闪退。

我执行的命令如下

# 指定设备
python dump_so.py -d 192.168.1.164:41309 -p com.shizhuanxxxxx.duapp -s libGameVMP.so

Multiple devices found:
1. 192.168.1.164:41309
2. adb-KCAIKN05L048ZAF-ovuptT._adb-tls-connect._tcp
Select device (1-2): 1
Started and attached to process: com.shizhuanxxxx.duapp (PID: 5913)
Frida script loaded
Looking for module: libGameVMP.so
Module not found, retry 1/10...
Found module info:
{'name': 'libGameVMP.so', 'version': None, 'base': '0x725d4cf000', 'size': 454656, 'path': '/data/app/~~HP4rmsQIdDjYodK-wFzpgg==/com.shizhuanxxxxx.duapp-53DwSEmI6IWzTVKI_jTzYg==/lib/arm64/libGameVMP.so'}
Starting dump of module: base=0x725d4cf000, size=454656
Saved dump file: libGameVMP.so.dump.so
Device architecture: arm64
android/SoFixer64: 1 file pushed, 0 skipped. 4.8 MB/s (2672240 bytes in 0.536s)
libGameVMP.so.dump.so: 1 file pushed, 0 skipped. 23.0 MB/s (454656 bytes in 0.019s)
adb -s 192.168.1.164:41309 shell /data/local/tmp/SoFixer -m 0x725d4cf000 -s /data/local/tmp/libGameVMP.so.dump.so -o /data/local/tmp/libGameVMP.so.dump.so.fix.so
[main_loop:87]start to rebuild elf file
[Load:69]dynamic segment have been found in loadable segment, argument baseso will be ignored.
[RebuildPhdr:25]=============LoadDynamicSectionFromBaseSource==========RebuildPhdr=========================
[RebuildPhdr:37]=====================RebuildPhdr End======================
[ReadSoInfo:552]=======================ReadSoInfo=========================
[ReadSoInfo:699]soname
[ReadSoInfo:624] constructors (DT_INIT) found at 20230
[ReadSoInfo:632] constructors (DT_INIT_ARRAY) found at 6b8e0
[ReadSoInfo:636] constructors (DT_INIT_ARRAYSZ) 35
[ReadSoInfo:640] destructors (DT_FINI_ARRAY) found at 6b9f8
[ReadSoInfo:644] destructors (DT_FINI_ARRAYSZ) 2
[ReadSoInfo:583]string table found at 10f0
[ReadSoInfo:587]symbol table found at 568
[ReadSoInfo:598] plt_rel_count (DT_PLTRELSZ) 123
[ReadSoInfo:594] plt_rel (DT_JMPREL) found at 2110
[ReadSoInfo:702]Unused DT entry: type 0x00000009 arg 0x00000018
[ReadSoInfo:702]Unused DT entry: type 0x00000018 arg 0x00000000
[ReadSoInfo:702]Unused DT entry: type 0x6ffffffb arg 0x00000001
[ReadSoInfo:702]Unused DT entry: type 0x6ffffffe arg 0x000015e8
[ReadSoInfo:702]Unused DT entry: type 0x6fffffff arg 0x00000003
[ReadSoInfo:702]Unused DT entry: type 0x6ffffff0 arg 0x000014ee
[ReadSoInfo:702]Unused DT entry: type 0x6ffffff9 arg 0x00000059
[ReadSoInfo:706]=======================ReadSoInfo End=========================
[RebuildShdr:42]=======================RebuildShdr=========================
[RebuildShdr:539]=====================RebuildShdr End======================
[RebuildRelocs:786]=======================RebuildRelocs=========================
[RebuildRelocs:812]=======================RebuildRelocs End=======================
[RebuildFin:712]=======================try to finish file rebuild =========================
[RebuildFin:736]=======================End=========================
[main:123]Done!!!
/data/local/tmp/libGameVMP.so.dump.so.fix....skipped. 4.3 MB/s (455601 bytes in 0.100s)
Fixed SO file: libGameVMP.so_0x725d4cf000_454656_fix.so
Cleaned up temporary files
PS C:\Users\21558\Downloads\frida_dump-master>

 

 

最后我们分析一下js关键代码

dump_so.js

// 定义RPC导出对象,这些函数可以被Python端调用
rpc.exports = {
    // 查找指定名称的模块函数
    findmodule: function (so_name) {
        // 使用Process.findModuleByName()在当前进程中查找指定名称的模块
        // so_name: 要查找的SO文件名,如"libnative.so"
        var libso = Process.findModuleByName(so_name);
        // 返回模块对象,包含name、base、size、path等信息
        // 如果找不到模块则返回null
        return libso;
    },

    // dump指定模块的内存数据函数
    dumpmodule: function (so_name) {
        // 首先查找指定名称的模块
        var libso = Process.findModuleByName(so_name);
        // 如果模块不存在,返回-1表示失败
        if (libso == null) {
            return -1;
        }

        // 修改内存保护属性为可读写执行(rwx)
        // 这是为了确保我们能够读取模块的所有内存区域
        // ptr(libso.base): 将基址转换为指针对象
        // libso.size: 模块的大小
        // 'rwx': 读(r)写(w)执行(x)权限
        Memory.protect(ptr(libso.base), libso.size, 'rwx');

        // 从模块基址开始读取整个模块的字节数据
        // ptr(libso.base): 模块在内存中的起始地址
        // readByteArray(libso.size): 读取指定大小的字节数组
        var libso_buffer = ptr(libso.base).readByteArray(libso.size);

        // 将读取的缓冲区数据附加到模块对象上(可选,用于调试)
        libso.buffer = libso_buffer;

        // 返回读取到的字节数组,这就是SO文件的完整内存映像
        return libso_buffer;
    },

    // 获取所有已加载模块的函数
    allmodule: function () {
        // Process.enumerateModules()返回当前进程中所有已加载模块的数组
        // 每个模块对象包含:name(名称)、base(基址)、size(大小)、path(路径)
        return Process.enumerateModules()
    },

    // 获取当前设备架构的函数
    arch: function () {
        // Process.arch返回当前进程的CPU架构
        // 可能的值:'arm', 'arm64', 'ia32', 'x64'
        // 这个信息用于选择正确的SoFixer工具版本
        return Process.arch;
    }
}

 

 

dump_dex.js

// 获取当前进程名称的函数
function get_self_process_name() {
    // 找到open导出函数
    var openPtr = Module.getExportByName('libc.so', 'open');
    // NativeFunction是c和js函数的桥梁。创建open函数的NativeFunction包装,参数:返回类型int,参数类型[pointer, int]。将一个已知地址的原生 C 函数 open,包装成一个可以被 JavaScript 直接、安全调用的 JavaScript 函数 open。它定义了如何转换参数和返回值,使得两个不同语言的世界能够无缝通信。
    var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);

    // 找到read导出函数
    var readPtr = Module.getExportByName("libc.so", "read");
    // 创建read函数的NativeFunction包装,参数:返回类型int,参数类型[int, pointer, int]
    var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);

    // 获取libc.so中close函数的地址
    var closePtr = Module.getExportByName('libc.so', 'close');
    // 创建close函数的NativeFunction包装,参数:返回类型int,参数类型[int]
    var close = new NativeFunction(closePtr, 'int', ['int']);

    // Memory.allocUtf8String() 的作用就是充当 js和 C串指针之间的桥梁。
    // 这个文件包含当前进程的命令行参数,第一个参数就是进程名
    var path = Memory.allocUtf8String("/proc/self/cmdline");
    // 打开文件,参数0表示只读模式
    var fd = open(path, 0);
    // 如果文件打开成功(文件描述符不等于-1)
    if (fd != -1) {
        // 分配4KB内存用于读取文件内容
        var buffer = Memory.alloc(0x1000);

        // 从文件中读取数据到缓冲区
        var result = read(fd, buffer, 0x1000);
        // 关闭文件
        close(fd);
        // 将缓冲区内容转换为C字符串并返回
        result = ptr(buffer).readCString();
        return result;
    }

    // 如果获取失败,返回"-1"
    return "-1";
}


// 创建目录
function mkdir(path) {
    // 获取libc.so中mkdir函数的地址
    var mkdirPtr = Module.getExportByName('libc.so', 'mkdir');
    // 创建mkdir函数的NativeFunction包装
    var mkdir = new NativeFunction(mkdirPtr, 'int', ['pointer', 'int']);

    // 获取libc.so中opendir函数的地址,用于检查目录是否存在
    var opendirPtr = Module.getExportByName('libc.so', 'opendir');
    // 创建opendir函数的NativeFunction包装
    var opendir = new NativeFunction(opendirPtr, 'pointer', ['pointer']);

    // 获取libc.so中closedir函数的地址
    var closedirPtr = Module.getExportByName('libc.so', 'closedir');
    // 创建closedir函数的NativeFunction包装
    var closedir = new NativeFunction(closedirPtr, 'int', ['pointer']);

    // 将js路径字符串转换为C字符串
    var cPath = Memory.allocUtf8String(path);
    // 尝试打开目录,检查是否存在
    var dir = opendir(cPath);
    // 如果目录存在(opendir返回非0值)
    if (dir != 0) {
        // 关闭目录句柄
        closedir(dir);
        // 目录已存在,直接返回
        return 0;
    }
    // 目录不存在,创建目录,权限设置为755(rwxr-xr-x)
    mkdir(cPath, 755);
    // 设置目录权限
    chmod(path);
}

// 修改文件/目录权限的函数
function chmod(path) {
    // 获取libc.so中chmod函数的地址
    var chmodPtr = Module.getExportByName('libc.so', 'chmod');
    // 创建chmod函数的NativeFunction包装
    var chmod = new NativeFunction(chmodPtr, 'int', ['pointer', 'int']);
    // 将路径字符串转换为C字符串
    var cPath = Memory.allocUtf8String(path);
    // 设置权限为755(rwxr-xr-x)
    chmod(cPath, 755);
}

// DEX文件dump的核心函数
function dump_dex() {
    // 查找libart.so模块,这是Android Runtime的核心库
    var libart = Process.findModuleByName("libart.so");
    // 初始化DefineClass函数地址为null
    var addr_DefineClass = null;
    // 枚举libart.so中的所有符号
    var symbols = libart.enumerateSymbols();
    // 遍历所有符号,查找DefineClass函数
    for (var index = 0; index < symbols.length; index++) {
        var symbol = symbols[index];
        var symbol_name = symbol.name;
        // 这个DefineClass的函数签名是Android9的
        // _ZN3art11ClassLinker11DefineClassEPNS_6ThreadEPKcmNS_6HandleINS_6mirror11ClassLoaderEEERKNS_7DexFileERKNS9_8ClassDefE
        // 通过符号名称特征匹配DefineClass函数

        // _ZN3art11ClassLinker11DefineClassEPNS_6ThreadEPKcmNS_6HandleINS_6mirror11ClassLoaderEEERKNS_7DexFileERKNS9_8ClassDefE

        // 拆解后就是:
        
        // 符号部分    含义(“地址”组成部分)    通俗解释
        // _ZN ... E    开始和结束标记    这是一个“修饰名”的包裹。
        // 3art    命名空间 (Namespace)    art。Android Runtime,就是安卓的系统核心。
        // 11ClassLinker    类名 (Class)    ClassLinker 类。这是ART里一个负责加载和链接类的“管理员”。
        // 11DefineClass    函数名 (Function)    DefineClass 方法。这是这个“管理员”的核心工作:定义一个类。
        // EPNS_6ThreadE    参数1 (Parameter 1)    art::Thread*。需要一个线程指针。就像办事要指明是哪个“工作人员”在处理。
        // PKc    参数2 (Parameter 2)    const char*。一个字符串,通常是类的描述符(如 "java/lang/String")。
        // m    参数3 (Parameter 3)    size_t。一个数字,表示上面字符串的长度。
        // PNS_6HandleINS_6mirror11ClassLoaderEEE    参数4 (Parameter 4)    art::Handle<art::mirror::ClassLoader>。一个类加载器对象的句柄。告诉系统用哪个“工具箱”(比如App自己的还是系统的)来加载这个类。
        // RKNS_7DexFileE    参数5 (Parameter 5)    const art::DexFile&。一个Dex文件的常量引用。这是最重要的参数!它告诉管理员:“请从这个DEX文件包裹里”取出类来。
        // RKNS9_8ClassDefE    参数6 (Parameter 6)    const art::DexFile::ClassDef&。一个类定义的常量引用。它进一步指明:“就取这个包裹里特定的那一份文件(类定义)”。
        // 所以,这个函数到底是干嘛的?
        // 它的核心工作就一件事:
        
        // 当一个Android App运行时,系统需要把DEX文件(打包好的Java代码)里的类加载到内存中才能执行。
        
        // 这个 DefineClass 函数就是ART虚拟机里负责这项工作的“首席加载官”。
        
        // 调用它的过程,就像是下指令:
        // “喂!ART系统的ClassLinker管理员!(3art11ClassLinker)
        // 现在请你 (11DefineClass):
        // 在当前这个线程 (EPNS_6ThreadE) 上,
        // 根据这个名字叫"com/example/MyClass" (PKc) 长度是XX (m) 的类,
        // 使用App提供的这个类加载器 (PNS_6HandleINS_6mirror11ClassLoaderEEE),
        // 从这个DEX文件里 (RKNS_7DexFileE),
        // 找到这个类的具体定义数据 (RKNS9_8ClassDefE),
        // 然后把它在内存里创建出来!”
        
        // 为什么Dump Dex的脚本要Hook它?
        // 这正是脚本聪明的地方!
        
        // 时机完美:这个函数被调用时,意味着系统正在主动地读取并加载一个DEX文件中的类。此时,整个DEX文件肯定已经完整地映射到内存中了。
        
        // 信息齐全:这个函数的参数就像一个“情报包”,直接包含了两个关键情报:
        
        // RKNS_7DexFileE: DexFile对象的内存地址。通过这个对象,脚本就能顺藤摸瓜找到DEX文件在内存中的起始地址 (begin_) 和大小 (size_)。
        
        // 这样,脚本就不需要漫无目的地搜索内存,而是在这个函数被调用时,直接“领取”了DEX文件的地址和大小,然后把它 dump 到磁盘上。
        
        // 总结一下:这个奇怪的字符串就是ART虚拟机里“加载类”这个核心功能员的完整身份证。Hook它,就能在最合适的时机、用最直接的方式,拿到我们想要dump的DEX文件的内存地址。
        
        


        if (symbol_name.indexOf("ClassLinker") >= 0 &&
            symbol_name.indexOf("DefineClass") >= 0 &&
            symbol_name.indexOf("Thread") >= 0 &&
            symbol_name.indexOf("DexFile") >= 0) {
            console.log(symbol_name, symbol.address);
            // 保存找到的DefineClass函数地址
            addr_DefineClass = symbol.address;
        }
    }
    // 用于存储已发现的DEX文件映射(基址->大小)
    var dex_maps = {};
    // DEX文件计数器,用于生成文件名
    var dex_count = 1;

    console.log("[DefineClass:]", addr_DefineClass);
    // 如果找到了DefineClass函数
    if (addr_DefineClass) {
        // hook DefineClass函数
        Interceptor.attach(addr_DefineClass, {
            // 函数调用前的回调
            onEnter: function (args) {

//  _ZN3art11ClassLinker11DefineClassEPNS_6ThreadEPKcmNS_6HandleINS_6mirror11ClassLoaderEEERKNS_7DexFileERKNS9_8ClassDefE     

//真实的参数列表是这样的:

// 序号    参数    对应 args[]
// 0    this (指向 ClassLinker 对象的指针)    args[0]
// 1    art::Thread* thread    args[1]
// 2    const char* descriptor    args[2]
// 3    size_t hash    args[3]
// 4    art::Handle... class_loader    args[4]
// 5    const art::DexFile& dex_file <-- 目标    args[5]
// 6    const art::DexFile::ClassDef& class_def    args[6]
// 结论:
// dex_file 参数在函数的正式参数列表中排在第5位(从0开始数),是因为它前面还有一个看不见的“第0号”参数——this 指针。

// 所以,args[5] 取到的就是传递给 DefineClass 函数的 dex_file 参数。

                var dex_file = args[5];

                // 这是DEX文件在内存中的起始地址
                var base = ptr(dex_file).add(Process.pointerSize).readPointer();
                // 这是DEX文件的大小
                var size = ptr(dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();

                // 如果这个DEX文件还没有被记录过
                if (dex_maps[base] == undefined) {
                    // 记录DEX文件的基址和大小
                    dex_maps[base] = size;
                    // 读取DEX文件的魔数(前几个字节)
                    var magic = ptr(base).readCString();
                    // 检查是否是有效的DEX文件(以"dex"开头)
                    if (magic.indexOf("dex") == 0) {
                        // 获取当前进程名
                        var process_name = get_self_process_name();
                        if (process_name != "-1") {
                            // 构建dump目录路径
                            var dex_dir_path = "/data/data/" + process_name + "/files/dump_dex_" + process_name;
                            // 创建dump目录
                            mkdir(dex_dir_path);
                            // 构建DEX文件路径,第一个文件名为class.dex,后续为class2.dex, class3.dex...
                            var dex_path = dex_dir_path + "/class" + (dex_count == 1 ? "" : dex_count) + ".dex";
                            console.log("[find dex]:", dex_path);
                            // 创建文件用于写入
                            var fd = new File(dex_path, "wb");
                            if (fd && fd != null) {
                                // 增加DEX文件计数
                                dex_count++;
                                // 从内存中读取完整的DEX文件数据
                                var dex_buffer = ptr(base).readByteArray(size);
                                // 写入文件
                                fd.write(dex_buffer);
                                // 刷新缓冲区
                                fd.flush();
                                // 关闭文件
                                fd.close();
                                console.log("[dump dex]:", dex_path);
                            }
                        }
                    }
                }
            },
            // 函数调用后的回调(这里为空)
            onLeave: function (retval) { }
        });
    }
}

// 标记是否已经hook了libart.so
var is_hook_libart = false;

// hook动态库加载函数的函数
function hook_dlopen() {
    // hook标准的dlopen函数   在较老版本的Android,或者一些非常规的、直接调用标准C库的场景中使用。
    Interceptor.attach(Module.findExportByName(null, "dlopen"), {
        // dlopen调用前的回调
        onEnter: function (args) {
            // args[0]是库文件路径参数
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                // 读取库文件路径字符串
                var path = ptr(pathptr).readCString();
                //console.log("dlopen:", path);
                // 如果正在加载libart.so
                if (path.indexOf("libart.so") >= 0) {
                    // 标记可以hook libart
                    this.can_hook_libart = true;
                    console.log("[dlopen:]", path);
                }
            }
        },
        // dlopen调用后的回调
        onLeave: function (retval) {
            // 如果可以hook libart且还没有hook过
            if (this.can_hook_libart && !is_hook_libart) {
                // 开始dump DEX文件
                dump_dex();
                // 标记已经hook过了
                is_hook_libart = true;
            }
        }
    })

    // hook Android特有的android_dlopen_ext函数
    //例如 Java 代码中加载原生库
// static {
//     System.loadLibrary("my-native-lib"); // 这会触发 dlopen 或 android_dlopen_ext
// }       在现代Android版本中,系统内部加载核心库(如 libart.so)时,更倾向于使用这个功能更强的函数。
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        // 调用前的回调
        onEnter: function (args) {
            // args[0]是库文件路径参数
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                // 读取库文件路径字符串
                var path = ptr(pathptr).readCString();
                //console.log("android_dlopen_ext:", path);
                // 如果正在加载libart.so
                if (path.indexOf("libart.so") >= 0) {
                    // 标记可以hook libart
                    this.can_hook_libart = true;
                    console.log("[android_dlopen_ext:]", path);
                }
            }
        },
        // android_dlopen_ext调用后的回调
        onLeave: function (retval) {
            // 如果可以hook libart且还没有hook过
            if (this.can_hook_libart && !is_hook_libart) {
                // 开始dump DEX文件
                dump_dex();
                // 标记已经hook过了
                is_hook_libart = true;
            }
        }
    });
}

// 立即执行dump_dex函数(如果libart.so已经加载)
setImmediate(dump_dex);

 

 

脱出所有dex和so的测试脚本

https://gitee.com/null_465_7266/dump-all-so/tree/master
使用方法
PS C:\Users\21558\Documents\dumpallso\dump-all-so> python dump_so.py -p com.shizhuang.duapp --app-path "/data/app/~~YxjKiMfU5GhbqDTDhPhhpw==/com.shizhuang.duapp-f6V4lziH2H4ySLWwb1S4_A==/lib/arm64

posted on 2025-08-19 13:30  GKLBB  阅读(261)  评论(0)    收藏  举报