为什么我要动态在内存中查找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

应用安全 --- frida脚本 之 dump 自动化动脱 so的更多相关文章

  1. 通过shell脚本实现代码自动化部署

    通过shell脚本实现代码自动化部署 一.传统部署方式及优缺点 1.传统部署方式 (1)纯手工scp (2)纯手工登录git pull.svn update (3)纯手工xftp往上拉 (4)开发给打 ...

  2. 基于脚本的modelsim自动化仿真笔记

    这里记录一下基于脚本的modelsim自动化仿真的一些知识和模板,以后忘记了可以到这里查找.转载请标明出处:http://www.cnblogs.com/IClearner/ . 一.基本介绍 这里介 ...

  3. 持续集成之⑤:jenkins结合脚本实现代码自动化部署及一键回滚至上一版本

    持续集成之⑤:jenkins结合脚本实现代码自动化部署及一键回滚至上一版本 一:本文通过jenkins调用shell脚本的的方式完成从Git服务器获取代码.打包.部署到web服务器.将web服务器从负 ...

  4. jenkins结合脚本实现代码自动化部署及一键回滚至上一版本

    持续集成之⑤:jenkins结合脚本实现代码自动化部署及一键回滚至上一版本 一:本文通过jenkins调用shell脚本的的方式完成从Git服务器获取代码.打包.部署到web服务器.将web服务器从负 ...

  5. awr脚本使用dump导出导入

    实际工作中,存在这么一种场景.客户现场分析问题,无法立即得出结论,且无法远程服务器,因此对于服务器中的awr信息,如何提取是一个问题,oracle有脚本可以对服务器中以db为单位导出awr基表的dum ...

  6. svn备份与还原_脚本_(dump命令)

    今天备份svn, 能保证好用就行先, 回头再研究 buerguo.bat @echo off :: 关闭回显 :: 说明:如有命令不明白,请使用帮助命令:命令/? .如:for/? :: 设置标题 t ...

  7. AutoIt脚本在做自动化操作的时候,如何进行错误捕获?

    我的自动化脚本在运行的时候,会生成一个界面,点击该页面上的按钮能够进行自动化操作. 经常遇到的一个问题是: 脚本运行一半,GUI程序出现了异常情况,这个时候,再次点击生成的界面上的按钮,不会有任何反应 ...

  8. Python脚本之——API自动化框架总结

    学完了Python脚本接口自动化之后,一直没有对该框架做总结,今天终于试着来做一份总结了. 框架结构如下图: 来说一下每个目录的作用: Configs:该目录下存放的是.conf,.ini文件格式的配 ...

  9. 你在和脚本谈恋爱(自动化在IM聊天中的应用)

    谢谢打开这篇文章的每个你 测开之分层自动化(Python)招生简章 Python自动化测试报告美化 在python中进行数据驱动测试 太嚣张了!他竟用Python绕过了“验证码” 在网络世界里你不知道 ...

  10. python脚本实现接口自动化轻松搞定上千条接口用例

    接口自动化目前是测试圈主流的一个话题,我也在网上搜索了很多关于自动化的关键词,大多数博主分享的python做接口自动化都是以开源的框架,比如:pytest.unittest+ddt(数据驱动) 最常见 ...

随机推荐

  1. win11 24h2系统更新后右键没反应的问题

    有雨林木风官网的用户,已经在电脑上更新到最新版win11 24h2系统了,但是更新后,却出现使用鼠标右键没有反应的问题,该如何解决呢?本文中,雨林木风小编将于大家分享详细的处理方法,有需要的朋友可以一 ...

  2. 如何在 Git 中控制某些文件不被提交?

    回答重点 在 Git 中控制某些文件不被提交的主要方法是使用 .gitignore 文件.通过在 .gitignore 文件中列出你不希望被提交的文件或文件夹路径,Git 就会自动忽略这些文件,不会将 ...

  3. Golang 文本模板,你指定没用过

    最近在倒腾"AI大模型基础设施", 宏观目标是做一个基于云原生的AI算力平台,目前因公司隐私暂不能公开宏观背景和技术方案, 姑且记录实践中遇到的一些技能点. Arena是阿里云开源 ...

  4. 虚拟机-Linux开发板交叉编译问题记录

    遇到一堆很久之前见过的问题,重新解决一次. 1.虚拟机没法上网 发现虚拟机浏览器上不了网,运行ifconfig查看,发现要么没有IP地址,要么只有IPv6的地址.最后发现是昨天VMware卡死了,启动 ...

  5. mdadm 和 LVM 存储管理工具区别

    mdadm 和 LVM 是 Linux 系统中两种不同的存储管理工具,​​核心目标和技术原理存在本质差异​​.虽然都涉及多块硬盘的管理,但解决的问题和应用场景截然不同.以下是详细对比及实际场景示例: ...

  6. 前端知识之CSS(3)-盒子模型、浮动布局、溢出属性、定位、脱离文档流、z-index之模态框

    目录 盒子模型 浮动布局(float) 1.什么是浮动 2.浮动的作用 3.浮动有俩个特点 4.浮动(float)格式 5.浮动会造成父标签塌陷 这是一个不好的现象 因为会引起歧义 6.解决父标签塌陷 ...

  7. [题解]P3200 [HNOI2009] 有趣的数列

    P3200 [HNOI2009] 有趣的数列 给出另一种转化思路,模拟赛的时候想到的. 将我们构造的序列看作 \(n\) 个点 \((a_1,a_2),(a_3,a_4),\dots,(a_{2n-1 ...

  8. [笔记]树形dp - 1/4(节点选择类)

    树形dp,是一种建立在树形结构上的dp,因此dfs一般是实现它的通用手段. 是一种很美的动态规划呢. P1352 没有上司的舞会 P1352 没有上司的舞会. 在一棵树中,找到若干个互相独立(即互相没 ...

  9. 修改linux ll 命令的日期显示格式

    0.ls -lh ( ll -h )查看文件的详细信息,显示的日期格式是英文,不直观. 1.vi  ~/.bash_profile,打开配置文件并增加环境变量.文件末尾添加这行 export TIME ...

  10. 洛谷 P2971 [USACO10HOL] Cow Politics G 题解

    怎么没有树上启发式合并的题解呢?我来发一篇吧! 简化题意 给定一棵 \(n\) 个点的树,每个点属于 \(k\) 种颜色之一(每种颜色至少有 2 个点).求每种颜色中,任意两点间的最大距离. 核心思想 ...