Frida学习笔记(二十三):SODump与内存Dump"

admin 2026-10-24 00:39:03 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Frida学习笔记(二十三):SO Dump 与内存 Dump

原创

泡泡以安
泡泡以安

泡泡以安

2026年6月26日 08:17
浙江

在小说阅读器读本章

去阅读

加固 SO 静态分析时 IDA 看到的是乱码,但代码总得在某一刻被还原成可执行的明文——这一篇讲的就是怎么在 SO 解密之后的窗口里把它整段读出来、修好 ELF 头给 IDA。配套的还有一类常见需求:dump 出运行时密钥/Token 的位置,顺手在 §4 一起讲。

一、为什么需要 SO Dump

1.1 加固对 SO 的保护

第26篇讲 DEX 脱壳(Java 层用 frida-dexdump 一行就能搞定)。Native 层就没有这么轻松——加固方案通常这么干:

保护方式 效果 静态分析能看到什么
SO 加密 APK 中的 SO 文件被加密,运行时由壳解密 IDA 打开看到乱码
SO 压缩 SO 被自定义压缩,运行时解压 IDA 无法识别 ELF 格式
代码段加密 SO 的 .text 段被加密,JNI\_OnLoad 中解密 IDA 能打开但函数全是乱码
字符串加密 SO 中的字符串被加密,运行时解密使用 IDA 中看不到有意义的字符串
导出表混淆 真实的导出函数名被替换 findExportByName 找不到函数

这些保护的共同特点是:运行时 SO 的代码必须被还原为可执行的明文状态——否则 CPU 就无法执行。Dump 的时机就是在这个还原完成之后。

1.2 Dump 的时机选择

SO 从加载到运行的整条生命周期上,有 4 个值得下手的窗口。没加固时任意时刻 dump 都行(下图窗口 ③);加固方案则需要更早下手——窗口 ② JNI\_OnLoad 返回是大多数场景的最佳时机,窗口 ④ 是 Anti-Dump 自毁前最后的窗口。

SO 加载生命周期与 4 个 dump 窗口

四种方案分别在 §2 / §5.1 / §5.2 / §5.3 展开。

二、方案一:从 /proc/self/maps 定位 + Memory.copy Dump

2.1 原理

Linux 系统中,/proc/self/maps 列出了进程的所有内存映射区域。SO 被加载后,其代码段(.text)和数据段(.data)会作为独立的映射区域出现在 maps 中。

2.2 完整的 SO Dump 脚本

// so_dump.js
// 从进程内存中 dump 指定的 SO 文件
// 用法: frida -U -n com.example.app -l so_dump.js

var TARGET_SO = \"libnative.so\";  // 要 dump 的 SO 名称

// 纯 Native 操作,无需 Java.perform 包装
(function () {
    var module = Process.findModuleByName(TARGET_SO);
    if (!module) {
        console.log(\"[!] \" + TARGET_SO + \" 未加载\");
        return;
    }

    console.log(\"[*] 找到 \" + TARGET_SO);
    console.log(\"  基地址: \" + module.base);
    console.log(\"  大小: \" + module.size + \" bytes (\" + (module.size / 1024).toFixed(1) + \" KB)\");
    console.log(\"  路径: \" + module.path);

    // 防止加固把 .text 设为 --x(仅可执行不可读)导致 readByteArray 触发 SIGSEGV
    Memory.protect(module.base, module.size, 'rwx');

    var buf = module.base.readByteArray(module.size);

    send({
        type: \"so_dump\",
        name: TARGET_SO,
        base: module.base.toString(),
        size: module.size
    }, buf);

    console.log(\"[*] SO 已发送到电脑端(\" + module.size + \" bytes)\");
})();

Python 端接收并保存:

# dump_receiver.py
import frida
import sys

def on_message(message, data):
    if message['type'] == 'send' and data:
        payload = message['payload']
        if payload.get('type') == 'so_dump':
            filename = payload['name'] + '.dump'
            with open(filename, 'wb') as f:
                f.write(data)
            print(f\"[*] 已保存: {filename} ({len(data)} bytes)\")
            print(f\"    基地址: {payload['base']}\")

device = frida.get_usb_device()
session = device.attach(\"com.example.app\")

with open(\"so_dump.js\") as f:
    script = session.create_script(f.read())

script.on('message', on_message)
script.load()

input(\"按回车退出...\")
session.detach()

2.3 精确 Dump:按内存段分别导出

上面的简单方式是一次性 dump 整个模块。但 SO 在内存中的布局可能和文件中不同(代码段和数据段之间可能有间隔)。更精确的方式是按段导出:

// so_dump_precise.js
// 精确的 SO dump:按照 /proc/self/maps 中的映射段导出

var TARGET_SO = \"libnative.so\";

(function() {
    var module = Process.findModuleByName(TARGET_SO);
    if (!module) {
        console.log(\"[!] \" + TARGET_SO + \" 未加载\");
        return;
    }

    console.log(\"[*] \" + TARGET_SO + \" @ \" + module.base + \" (\" + module.size + \" bytes)\
\");

    // === Step 1: 枚举模块的所有可读段 ===
    var ranges = module.enumerateRanges(\"r--\");
    console.log(\"[*] 内存段 (\" + ranges.length + \" 个):\");
    ranges.forEach(function(range, i) {
        var offset = range.base.sub(module.base);
        console.log(\"  段\" + i + \": \" + range.base + \" - \" + range.base.add(range.size) +
                    \" (\" + range.protection + \") \" + range.size + \" bytes\" +
                    \" (偏移 0x\" + offset.toString(16) + \")\");
    });

    // === Step 2: 计算合并后的总大小(最后一段末尾 - 基址) ===
    var lastRange = ranges[ranges.length - 1];
    var totalSize = lastRange.base.add(lastRange.size).sub(module.base).toInt32();
    console.log(\"\
[*] 总大小: \" + totalSize + \" bytes\");

    // === Step 3: 分配输出缓冲区 ===
    // Memory.alloc 返回的页是零初始化的,不需要再 fill;
    // 老教程里 `new Array(size).fill(0)` 在 2 MB+ 模块上会卡几秒。
    var outputBuf = Memory.alloc(totalSize);

    // === Step 4: 逐段拷贝到输出缓冲区 ===
    ranges.forEach(function(range) {
        var offset = range.base.sub(module.base).toInt32();
        try {
            Memory.copy(outputBuf.add(offset), range.base, range.size);
        } catch(e) {
            console.log(\"  [!] 段复制失败 @ \" + range.base + \": \" + e.message);
        }
    });

    // === Step 5: 一次性发送 ===
    var dumpData = outputBuf.readByteArray(totalSize);
    send({ type: \"so_dump\", name: TARGET_SO, size: totalSize }, dumpData);
    console.log(\"\
[*] Dump 完成,已发送\");
})();

上面两个脚本默认目标 SO 已经在内存里。如果 SO 是 App 启动后才动态加载的(常见于加固 SO),或者你想精确卡在 JNI\_OnLoad 解密结束的瞬间,看 §5.1。

三、ELF 修复:让 dump 出来的 SO 能进 IDA

3.1 为什么 dump 的 SO 可能无法直接分析

从内存 dump 出的 SO 和原始的 SO 文件有几个关键区别:

  1. Section Header Table 缺失或损坏:linker 在加载 SO 时只使用 Program Header(段信息),不需要 Section Header(节信息)。但 IDA 的分析依赖 Section Header 来划分代码区域和数据区域。
  2. 地址重定位:内存中的 SO 已经经过了重定位(基地址 + 偏移),而 IDA 期望的是未重定位的文件格式。
  3. 空白填充:内存映射中段与段之间的空隙被零填充,但文件格式中这些空隙不存在。

APK 中的 SO vs 内存 dump 出的 SO 布局对比

3.2 使用 SoFixer 修复

SoFixer 是一个专门修复内存 dump SO 的工具:

# 使用 SoFixer 修复
# 参数: -s 源文件 -o 输出文件 -m 基地址 -d (dump模式)
./SoFixer -s libnative.so.dump -o libnative_fixed.so -m 0x7a8b000000 -d

基地址从 Frida dump 脚本的输出中获取。

3.3 兜底:30 行的极简修复版

§3.2 的 SoFixer 通常够用——下面这个版本是为了让你看明白「修复在做什么」(其实就是清几个字段),也作为 SoFixer 报错时的兜底:

# fix_elf.py
# 简单的 ELF Section Header 修复,兼容 ELF32 / ELF64
import struct

# ELF32 与 ELF64 的 e_shoff / e_shnum / e_shstrndx 偏移与宽度不同
LAYOUT = {
    1: {\"shoff\": (32, '<I', 4), \"shnum\": (48, '<H', 2), \"shstrndx\": (50, '<H', 2)},  # ELFCLASS32
    2: {\"shoff\": (40, '<Q', 8), \"shnum\": (60, '<H', 2), \"shstrndx\": (62, '<H', 2)},  # ELFCLASS64
}

def fix_dump(input_path, output_path):
    with open(input_path, 'rb') as f:
        data = bytearray(f.read())

    if data[:4] != b'\\x7fELF':
        print(\"[!] 不是有效的 ELF 文件\")
        return

    ei_class = data[4]  # 1=ELF32, 2=ELF64
    if ei_class not in LAYOUT:
        print(f\"[!] 未知 EI_CLASS={ei_class}\")
        return
    L = LAYOUT[ei_class]
    print(f\"[*] ELF{'32' if ei_class == 1 else '64'}\")

    e_shoff = struct.unpack_from(L[\"shoff\"][1], data, L[\"shoff\"][0])[0]
    e_shnum = struct.unpack_from(L[\"shnum\"][1], data, L[\"shnum\"][0])[0]
    e_shstrndx = struct.unpack_from(L[\"shstrndx\"][1], data, L[\"shstrndx\"][0])[0]
    print(f\"    Section Header: offset=0x{e_shoff:x}, num={e_shnum}, strndx={e_shstrndx}\")

    # Section Header 偏移指向文件外(dump 后变小) → 清零,让 IDA 走 Program Header 路径
    if e_shoff > len(data) or e_shnum == 0:
        print(\"[*] 清理无效的 Section Header 引用\")
        struct.pack_into(L[\"shoff\"][1],    data, L[\"shoff\"][0],    0)
        struct.pack_into(L[\"shnum\"][1],    data, L[\"shnum\"][0],    0)
        struct.pack_into(L[\"shstrndx\"][1], data, L[\"shstrndx\"][0], 0)

    with open(output_path, 'wb') as f:
        f.write(data)
    print(f\"[*] 修复完成: {output_path}\")
    print(f\"    IDA: File → Load file → ELF (勾 'Manual load' + 'Force load')\")

fix_dump(\"libnative.so.dump\", \"libnative_fixed.so\")

32-bit SO 提醒:如果你的目标 App 还在跑 armeabi-v7a(老游戏 / 海外 App / 加固兜底),IDA 处加载时记得选 ARM Little-endian 32-bit;SoFixer 同一条命令照用,无需切工具。

IDA 加载技巧:打开修复后的 SO 时,IDA 可能会报警告。选择「Manual load」并勾选「Force load」可以强制加载。即使 Section Header 缺失,IDA 仍然能基于 Program Header 进行反汇编和伪代码生成——只是函数名识别和字符串引用可能不如完整 SO 精确。

四、配套场景:用第 14 篇的算法自吐定位密钥位置

第 14 篇的「算法自吐」能让你拿到密钥的值,但值在哪里产生才是更值钱的信息——判断密钥是硬编码常量、SO 数据段被解出的、还是堆上运行时计算的,直接决定后续怎么搞:硬编码就直接静态拿,运行时生成就要 Hook 派生函数。

// dump_key_location.js
var KNOWN_KEY_HEX = \"0123456789abcdef0123456789abcdef\";  // 算法自吐拿到的 16 字节 AES key

(function () {
    var pattern = KNOWN_KEY_HEX.match(/.{2}/g).join(\" \");
    Process.enumerateRanges(\"r--\").forEach(function (range) {
        try {
            Memory.scanSync(range.base, range.size, pattern).forEach(function (match) {
                var mod = Process.findModuleByAddress(match.address);
                var loc = mod ? mod.name + \"+0x\" + match.address.sub(mod.base).toString(16)
                              : \"堆/栈 @ \" + match.address;
                console.log(\"\
[KEY] \" + loc + \"  prot=\" + range.protection);

                if (range.protection.indexOf(\"x\") !== -1)         console.log(\"  → 代码段:硬编码常量\");
                else if (mod && mod.path.indexOf(\"/data/\") === 0) console.log(\"  → App SO 数据段:可能是初始化时解出的\");
                else if (!mod)                                    console.log(\"  → 堆/栈:运行时派生\");
                console.log(hexdump(match.address.sub(32), { length: 96 }));
            });
        } catch (e) {}
    });
})();

通用的「全进程搜字符串/特征码」Memory.scanSync 用法跟上面一样,改 pattern 即可——比如搜 AES S-Box 63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76 定位加密实现。

五、精准定位解密时机:在加固 SO 真正「脱壳」的瞬间下手

前面说「任意时刻 dump」对没加固的 SO 够用,但对二次解密、SO 自毁(Anti-Dump)、代码段重新加密等高级保护无效——你 dump 到的可能是早已被擦除的中间态,或者根本读不到 .text。下面给三种「精确踩点」。

§5.1 和 §5.3 都会调一个公共 dump 函数,先粘在这:

// 强制 rwx 再 read,避免 --x 段触发 SIGSEGV;
// 一次整块读,段间空隙交给 SoFixer 处理。
function dumpAndSend(soName) {
    var m = Process.findModuleByName(soName);
    if (!m) { console.log(\"[!] \" + soName + \" 未加载\"); return; }
    Memory.protect(m.base, m.size, 'rwx');
    send({ type: \"so_dump\", name: soName,
           base: m.base.toString(), size: m.size },
         m.base.readByteArray(m.size));
}

5.1 Hook JNI\_OnLoad onLeave

很多加固在 JNI_OnLoad 里完成最后一轮解密。早期的简化做法是 dlopen onLeave 后 setTimeout(500ms) 等 JNI\_OnLoad 大概率跑完——能用,但不准。准确做法是 Hook JNI_OnLoad 它自己的 onLeave:dlopen 返回之后立刻 dump 太早(可能拿到部分加密版),JNI\_OnLoad 进入之后立刻 dump 又太晚(解密还没做),返回那一刻正好。

// dump_after_jnionload.js
function hookJniOnLoadReturn(soName) {
    var hooked = false;
    [\"dlopen\", \"android_dlopen_ext\"].forEach(function (name) {
        Interceptor.attach(Module.findExportByName(null, name), {
            onEnter: function (args) {
                this.path = args[0].readCString();
            },
            onLeave: function (retval) {
                if (hooked) return;
                var path = this.path || \"\";
                if (path.indexOf(soName) === -1) return;

                // ⚠ 不要用 Process.findModuleByAddress(retval):
                // Android 10+ Bionic 对 dlopen 句柄做了加密混淆,
                // retval 不是模块基址,会拿到 null。用 SO 名字找模块才稳:
                var mod = Process.findModuleByName(soName);
                if (!mod) return;

                var jniOnLoad = Module.findExportByName(soName, \"JNI_OnLoad\");
                if (!jniOnLoad) return;

                hooked = true;
                Interceptor.attach(jniOnLoad, {
                    onLeave: function () {
                        // 这是 .text 段最稳定的\"明文窗口\"
                        console.log(\"[*] \" + soName + \" JNI_OnLoad returned, dumping now\");
                        dumpAndSend(soName);
                    }
                });
            }
        });
    });
}
hookJniOnLoadReturn(\"libsecure.so\");

Android 10+ 别用 findModuleByAddress(retval):Bionic(Android 的 libc)把 dlopen 句柄做了加密混淆,它不是模块基址,永远找不到模块。从 path 提取 SO 名再 findModuleByName 才稳。

Android arm64 user 空间 vs dlopen 句柄实测值

5.2 用写入断点定位解密循环

如果加固没集中到 JNI_OnLoad、而是「边执行边解密」(像 OLLVM + 自定义 .text 段加密),可以反着抓——先让 .text 不可读,等谁来解密时被中断、就拿到了解密代码:

// catch_decrypt_routine.js
// 思路:把 .text 设为不可读,等访问异常被触发,看是谁触发的
var TARGET = \"libsecure.so\";
var mod = Process.findModuleByName(TARGET);
if (mod) {
    var textSeg = mod.enumerateRanges(\"r-x\")[0];   // 首段通常是 .text
    Memory.protect(textSeg.base, textSeg.size, \"---\");

    Process.setExceptionHandler(function (ex) {
        if (ex.type === \"access-violation\"
            && ex.memory && ex.memory.address
            && ex.memory.address.compare(textSeg.base) >= 0
            && ex.memory.address.compare(textSeg.base.add(textSeg.size)) < 0) {

            console.log(\"[!] decrypt routine PC=\" + ex.context.pc
                      + \"  访问 .text @ \" + ex.memory.address);
            // 恢复保护,让程序继续执行
            Memory.protect(textSeg.base, textSeg.size, \"rwx\");
            return true;   // 异常已处理
        }
        return false;
    });
}

打印的 PC 就是壳的解密例程入口。把它在 IDA 里定位,能看清整个解密算法。

ex.memory.address 必须按 .text 范围过滤:不过滤就完全淹没在 ART 的 write-barrier / card-table / JIT 良性缺页里。另外 enumerateRanges('r-x')[0] 只对单段 .text 的简单 SO 安全,多段 .text 的加固方案要遍历所有 r-x 段。

setExceptionHandler:地址过滤前后的信噪比对比

5.3 检测 SO 自毁,赶在擦除之前 dump

部分加固会在主线程稳定运行后主动擦除原始 .text 字节(Anti-Dump)。两条对策:

// 对策 A:JNI_OnLoad 一返回就立即 dump(时间窗口最大)
// 见 §5.1

// 对策 B:监控 mprotect/mremap,看谁在改 .text 段权限
var mprotect = Module.findExportByName(\"libc.so\", \"mprotect\");
Interceptor.attach(mprotect, {
    onEnter: function (args) {
        var addr = args[0];
        var size = args[1].toInt32();
        var prot = args[2].toInt32();
        var mod = Process.findModuleByAddress(addr);
        if (mod && mod.name === \"libsecure.so\") {
            console.log(\"[mprotect] \" + addr + \" size=0x\" + size.toString(16)
                      + \" prot=\" + prot + \"  by \" + DebugSymbol.fromAddress(this.returnAddress));
            // PROT_READ=1 / PROT_WRITE=2 / PROT_EXEC=4
            // 若即将丢失可读位 → Anti-dump 擦除前最后窗口,立即 dump
            if ((prot & 1) === 0) {
                dumpAndSend(\"libsecure.so\");
            }
        }
    }
});

为什么是 (prot & 1) === 0 而不是 (prot & 4) === 0?下面这张矩阵把 8 种 prot 取值与两种写法逐列对比,真机实测命中的 0x3 [rw-] 和 0x0 [---] 用淡黄高亮——能直接看到老教程写法在 [rw-] 上的 50% 误报率:

mprotect prot 参数:两种位检查的对比矩阵

六、实战 Case:dump 抖音 libmetasec_ml.so

抖音的核心加密在 libmetasec_ml.so(metasec 是字节内部安全 SDK MetaSec 的命名前缀,ml 指其机器学习风控子模块)。这个 SO 不被打包到 APK,而是从一个内嵌资源在运行时解密、写入 /data/data/.../app_msec/ 目录后再 dlopen。直接从 APK 提取一定是空的,必须 dump。

下面这两个文件就是配套仓库 scripts/frida23/ 里 24 行的精简版(改写自 lasting-yang/frida\_dump)。

6.1 工具源码(24 行能跑的版本)

// dump_so.js —— 仓库原版
rpc.exports = {
    findmodule: function (so_name) {
        return Process.findModuleByName(so_name);
    },
    dumpmodule: function (so_name) {
        var libso = Process.findModuleByName(so_name);
        if (libso == null) return -1;
        Memory.protect(ptr(libso.base), libso.size, 'rwx');     // ← 关键
        return ptr(libso.base).readByteArray(libso.size);
    },
};
# dump_so.py —— 仓库原版(精简)
import frida, sys

JS = open(\"dump_so.js\").read()

def dump(so_name):
    device = frida.get_usb_device()
    pid = device.get_frontmost_application().pid
    session = device.attach(pid)
    script = session.create_script(JS)
    script.load()

    info = script.exports.findmodule(so_name)
    print(f\"base={info['base']} size={info['size']}\")
    buf = script.exports.dumpmodule(so_name)
    if buf != -1:
        with open(so_name + \".dump.so\", \"wb\") as f:
            f.write(buf)
        print(f\"wrote {so_name}.dump.so ({len(buf)} bytes)\")

if __name__ == \"__main__\":
    dump(sys.argv[1] if len(sys.argv) > 1 else \"libmetasec_ml.so\")

24 行就是一把「够用的 SO dump 枪」。它的几个关键决策值得拆一下:

  1. 用 RPC 把 dump 操作做成 Python 主导,不依赖 send 双端协议——script.exports.dumpmodule(...) 一行同步拿到字节数组;
  2. Memory.protect(base, size, 'rwx') 强制把模块标记为可读——很多加固会在 JNI_OnLoad 后把 .text 改成 --x(仅可执行不可读),不强行 protect 直接 readByteArray 会触发 SIGSEGV;
  3. 把整个模块视为一段连续内存(不区分段)一次读出,速度比逐段拼接快数倍。代价是模块中段间空隙会被零填充——这就是为什么后面 SoFixer 一定要跑。

6.2 操作步骤(实测)

# 1. 启动抖音 lite,等首页加载完毕(msec ML 已 dlopen)
adb shell am start -n com.ss.android.ugc.aweme.lite/.main.MainActivity

# 2. 跑 dump (假设 dump_so.js / dump_so.py 在当前目录)
python dump_so.py libmetasec_ml.so
# 输出: base=0x7b3c004000 size=2228224
#       wrote libmetasec_ml.so.dump.so (2228224 bytes)

# 3. 用 SoFixer 修 ELF (从 https://github.com/F8LEFT/SoFixer 拿 release 二进制)
SoFixer -s libmetasec_ml.so.dump.so \\
        -o libmetasec_ml.fixed.so \\
        -m 0x7b3c004000 \\
        -d

记下 base 0x7b3c004000,IDA 的 Manual load 弹窗里要用。

6.3 别跳过这一步:IDA 二次验证

不要相信「SoFixer 输出 success 就 OK」——很多 dump 在 IDA 里还是会出现 bad fixup、unresolvable reloc、伪代码全是 byte_xxx。验证步骤:

步骤 操作 通过标准
① 加载 IDA → File → Open → libmetasec_ml.fixed.so,弹窗里勾 Manual load 能选择 Processor type 为 ARM Little-endian (ARM64)
② Loading offset 弹窗「Loading address」填0(不是 dump 时的基址) IDA 完成初始化无报错
③ 段识别 View → Open subviews → Segments 至少能看到 LOAD / .text / .dynamic 三段
④ 字符串 View → Open subviews → Strings (Shift+F12) 能看到正常字符串(如 \"libmetasec_ml\"、AES IV、域名等);如果全是乱码,说明二次解密还没完成,回到 §5.1 重抓
⑤ 导出表 Exports 窗口(Ctrl+E) 能看到 JNI_OnLoad、amazonaws_xxx 等真实导出名
⑥ 反汇编 跳到 JNI_OnLoad,按 F5 能生成伪代码且包含可读字符串引用(不是 *(char *)0xXXXXXX)
⑦ 交叉引用 在伪代码里右键某个函数 → Jump to xref 能跳到调用方

如果 ⑥ 失败:通常是 SoFixer 的 -m 基址写错了,或者 dump 时 JNI_OnLoad 还没执行完。回到 §5.1 用 JNI\_OnLoad onLeave 再 dump 一次。

6.4 反向验证:拿 dump 出的 SO 跑 frida-trace

最有力的「dump 是否成功」验证不是看 IDA,而是用 dump 出的 SO 推导出函数偏移、然后回到运行中的 App 用 frida-trace 验证它:

# 假设你在 IDA 里看到关键函数在 fixed SO 里偏移 0x1A340
frida-trace -U -n com.ss.android.ugc.aweme.lite \\
    -a \"libmetasec_ml.so!0x1A340\"

# 如果触发到了、参数能打印——dump + IDA 分析链路全打通

6.5 从 24 行到通用工具:lasting-yang/frida\_dump 的演进

上面那 24 行就是 lasting-yang/frida\_dump 的内核。完整社区版多了 allmodule() 枚举、批量 dump、dump_dex.js(参考第 26 篇)等扩展,但核心还是同一把「枪」。值得抄过来的三个工程决策:

  1. RPC vs send:双向异步 send/recv 适合流式数据;批量 dump 这种「我问你答」用 RPC,一行 script.exports.dumpmodule(...) 同步拿字节数组,省去 on('message') 回调和缓冲。
  2. Memory.protect(rwx) 在 read 之前:很多脚本漏了,遇到 --x 段直接 SIGSEGV。
  3. 不做 ELF 修复,留给 SoFixer:单一职责。两步用 shell pipe 串起来即可,不要做大而全。

总结

场景 方案 时机
标准 SO(无加固) Process.findModuleByName + readByteArray 任意时刻
动态加载的 SO Hook JNI_OnLoad onLeave(见 §5.1) JNI\_OnLoad 返回
加固 SO(有 JNI\_OnLoad 二次解密) 同上 onLeave
加固 SO(边跑边解密) Memory.protect(.text, '---') + 异常处理器 触发缺页时
加固 SO(Anti-Dump 自毁) 监控 mprotect/mremap,赶在擦除前 dump 检测到权限变更
配套需求:定位运行时密钥 Memory.scanSync 已知值 + 内存属性归类 算法自吐拿到 key 后

dump 完不是终点,IDA 七步验证才是终点。如果 IDA 中 JNI_OnLoad 的 F5 伪代码没有可读字符串引用,回到 §5.1 重抓——这是最常见的「以为 dump 成功了其实没成」。


Frida学习笔记(二十三):SODump与内存Dump" 网络安全文章

Frida学习笔记(二十三):SODump与内存Dump"

本文详细介绍了使用Frida进行SO文件内存Dump的技术方法,重点解决了加固SO在解密后如何完整提取并修复ELF头以供IDA分析的问题。文章提供了四种Dump时机选择策略,给出了完整的Frida脚本实现,包括基础Dump和精确分段Dump两种方案,并配套Python接收代码。关键发现包括通过/proc/self/maps定位内存区域、使用Memory.copy进行数据提取,以及使用SoFixer工具或自定义Python脚本修复SectionHeaderTable损坏等ELF格式问题。可操作建议涵盖动态加
逆向某AIApp:设备注册与AI对话的完整协议分析" 网络安全文章

逆向某AIApp:设备注册与AI对话的完整协议分析"

本文详细逆向分析了某AI应用iOS端的设备注册与AI对话协议,核心发现包括:设备注册接口不校验签名但依赖特定设备指纹组合与TTEncrypt加密,AI对话接口则强制校验签名;通过动态模拟执行成功绕过OLLVM混淆的签名算法,实现了纯协议层的新设备注册与有效会话获取。
面试官视角的渗透测试技术面" 网络安全文章

面试官视角的渗透测试技术面"

该文档从面试官视角分享渗透测试技术面经验,涵盖面试题选择标准(网络题库、工作实践、简历技能)、分级评估方法(初级/中级/高级)及岗位匹配逻辑。指出面试结果受能力与运气双重影响(如项目需求变化、HC调整),并提供渗透测试、WEB安全、移动端(Android/iOS)等具体考察方向。建议求职者以基础能力匹配岗位薪资预期,无需过度焦虑超纲问题。
评论:0   参与:  0