首页
社区
课程
招聘
[原创] Frida 17.6 最新Hook注入方案分析(Zymbiote注入机制)
发表于: 1天前 719

[原创] Frida 17.6 最新Hook注入方案分析(Zymbiote注入机制)

1天前
719

本文主要是想要了解清楚 Frida 在新版本有什么地方不一样了。本文主要还是着重在 Zymbiote的注入机制上,其他相关板块后续再出文章分析。

然后在阅读 Frida 源码的过程也发现了很多有意思的小trick,鉴于本人水平有限如果有问题请各位大佬指出。

版本说明:

Frida 17.6

参考文章:

  • b39K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9K6c8W2)9#2k6W2)9#2k6X3u0A6P5W2)9K6c8p5#2*7g2e0y4y4g2q4V1#2e0i4A6c8P5p5#2m8i4K6y4p5i4K6y4p5i4K6t1$3j5h3#2H3i4K6y4n7L8h3W2V1i4K6y4p5x3U0t1@1y4K6b7^5y4e0l9H3x3W2)9J5y4X3q4E0M7q4)9K6b7X3W2V1P5q4)9K6c8o6q4Q4x3U0k6S2L8i4m8Q4x3@1u0K6L8W2)9K6c8o6q4X3x3r3f1%4x3o6M7%4k6U0p5&6x3U0M7H3x3$3c8T1j5K6l9%4z5e0x3#2j5e0j5K6x3e0p5K6z5e0R3H3i4K6t1$3j5h3#2H3i4K6y4n7j5$3S2C8M7$3#2Q4x3@1c8X3k6o6y4X3y4o6f1%4x3h3p5$3x3K6j5K6k6e0k6X3z5e0x3$3z5r3q4S2x3e0R3H3z5o6M7$3j5K6j5H3x3U0N6U0j5e0t1#2k6U0l9@1z5h3t1#2x3U0l9K6x3o6f1K6z5h3q4V1j5h3f1%4k6o6b7&6x3U0c8T1z5e0R3%4y4o6V1H3y4h3x3%4z5o6p5$3y4r3b7I4i4K6t1$3j5h3#2H3i4K6y4n7L8i4m8K6K9r3q4J5k6g2)9K6c8o6q4Q4x3U0k6S2L8i4m8Q4x3@1u0K6j5$3g2F1k6g2)9K6c8o6t1K6i4K6t1$3j5h3#2H3i4K6y4n7M7%4u0U0K9h3c8Q4x3@1b7H3x3e0t1I4d9q4N6V1e0Y4f1K6d9@1y4W2b7Y4y4X3e0#2t1%4k6s2y4%4k6U0k6Q4x3U0k6S2L8i4m8Q4x3@1u0K6K9r3q4J5k6i4u0Q4y4h3k6K6K9r3q4J5k6h3W2F1k6X3!0Q4x3@1b7H3z5e0f1%4z5h3x3J5y4K6l9#2x3$3t1%4j5e0m8S2j5e0u0W2k6r3j5@1z5o6V1H3y4U0f1@1x3o6t1^5x3q4)9J5y4X3q4E0M7q4)9K6b7Y4y4Z5j5i4u0W2M7W2)9#2k6Y4y4Z5j5i4u0W2K9h3&6X3L8#2)9#2k6X3k6A6M7Y4y4@1i4K6y4p5j5$3p5&6z5e0m8X3k6r3b7I4z5r3t1I4z5h3q4S2k6o6q4T1j5e0W2U0z5o6M7K6z5o6p5@1z5h3u0X3k6r3u0Q4x3U0y4J5k6l9`.`.

目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. 前言
   1.1 版本说明
   1.2 参考文章
 
2. 历史Hook方案
   2.1 旧方案问题
   2.2 新方案
 
3. 流程图
 
4. Zymbiote注入阶段
   4.1 函数调用链
   4.2 补丁
       4.2.1 Payload补丁
       4.2.2 ArtMethod补丁
   4.3 Boot Heap
       4.3.1 Boot Image文件
   4.4 ensure_loaded (初始化)
   4.5 inject_zymbiote (核心注入逻辑)
   4.6 do_prepare_zymbiote_injection(核心准备逻辑)
       4.6.1 调用链
       4.6.2 初始化变量
       4.6.3 解析进程内存映射
       4.6.4 获取libc与runtime信息
       4.6.5 定位ArtMethod信息
       4.6.6 填充payload

历史Hook方案

旧方案问题

  • 需要 Zygote 内注入 agent
    • 需要 ptrace 暂停/恢复线程
    • 在所有子进程中留下痕迹
    • 需要隐藏 FD,否则 Zygote 会 abort
  • 依赖于 syscall tracing
    • Ptrace 的 syscall tracing 逻辑复杂且脆弱
  • Teardown 路径不安全
    • 难以保证在各种时机正确回收
    • 容易在 Zygote 和 system_server 中造成不稳定

新方案

  • 完全外部化
    • 不注入 Zygote agent
    • 通过 /proc/$pid/mem 写入小型 payload
    • 不使用 ptrace
  • 短生命周期设计
    • Payload 仅负责握手和暂停
    • 执行后立即回滚,不留常驻代码
  • 利用稳定入口点
    • 使用 android.os.Process.setArgV0Native() 作为触发点
    • 该函数在子进程启动时必然被调用

流程图

图片描述

Zymbiote 注入阶段

Zymbiote 是 Frida 在 Android 上实现轻量级 Zygote Hook 的机制,用于在应用进程启动时注入 Frida

命名来源:Zymbiote = Zygote + Symbiote(共生体),表示与 Zygote 进程共生。

函数调用链

enable_spawn_gating() (linux-host-session.vala:261)
  └─ robo_launcher.enable_spawn_gating() (linux-host-session.vala:263)
      └─ ensure_loaded() (linux-host-session.vala:661)
          ├─ 创建 Unix Socket 服务器
          ├─ handle_zymbiote_connections.begin() (linux-host-session.vala:785)
          └─ 枚举 Zygote 进程
              └─ do_inject_zygote_agent.begin() (linux-host-session.vala:816)
                  └─ inject_zymbiote() (linux-host-session.vala:838)
                      ├─ prepare_zymbiote_injection() (linux-host-session.vala:847)
                      │   └─ do_prepare_zymbiote_injection() (linux-host-session.vala:905)
                      │       ├─ 解析 /proc/<pid>/maps
                      │       ├─ 定位 payload 基址 (libstagefright.so)
                      │       ├─ 解析 libc.so 和 libandroid_runtime.so
                      │       ├─ 定位 ArtMethod 槽位
                      │       └─ 准备 payload 数据
                      ├─ 暂停 Zygote 进程 (SIGSTOP)
                      ├─ 应用补丁 (ZymbiotePatches.apply)
                      │   ├─ 写入 payload 到可执行映射
                      │   └─ 替换 ArtMethod 槽位指针
                      └─ 恢复 Zygote 进程 (SIGCONT)

[子进程 fork 后]
  └─ frida_zymbiote_replacement_set_argv0() (zymbiote.c:44)
      ├─ 回滚 ArtMethod 槽位
      ├─ 调用原始 setArgV0Native
      ├─ 连接 Unix Socket
      ├─ 发送 Hello 消息
      ├─ 等待 ACK
      └─ 触发 SIGSTOP

[Frida Core 处理]
  └─ handle_zymbiote_connection() (linux-host-session.vala:1128)
      ├─ read_hello() (linux-host-session.vala:1130)
      ├─ 判断是否需要 spawn gating
      └─ connection.resume() (linux-host-session.vala:1150)
          ├─ 发送 ACK
          ├─ 等待进程停止
          ├─ 回滚子进程补丁
          └─ 发送 SIGCONT

补丁

后文会出现补丁相关的代码,所以这里解释一下。其实很好理解,这里补丁就是我们patch上去的代码

补丁目前分为两种类型

Payload 补丁

这里很好理解,payload相当于我们传入的二进制代码,然后我们在 ArtMethod 改对应属性指向我们的payload,这样他们执行函数的时候就会先执行我们的payload

  • 内容:编译后的 zymbiote 二进制代码(约 800-900 字节)
  • 位置:libstagefright.so 的最后一页(可执行映射)(这里为什么选择这个so捏,因为这是系统库,通常已加载)
  • 作用:在目标进程中放置可执行代码
// 将 payload 代码写入到可执行内存
patches.apply(payload, process_memory, payload_base);

ArtMethod 补丁

  • 内容:指针值(4 或 8 字节)
  • 位置:boot heap 中的 ArtMethod 槽位
  • 作用:将函数指针从原始函数改为指向 payload
// 替换 ArtMethod 槽位指针
patches.apply(replaced_ptr, process_memory, art_method_slot);

Boot Heap

这个概念在后面也会出现,我们这里可以解释一下。

Boot Heap = Android ART 运行时在启动时预加载的内存区域。

Boot Heap 存储:
├─ 预编译的 Android 框架类
├─ ArtMethod 对象(包括 setArgV0Native 的 ArtMethod)
├─ 预加载的类元数据
└─ 系统库的方法信息
private static bool is_boot_heap (string path) {
    return
        "boot.art" in path ||
        "boot-framework.art" in path ||
        "dalvik-LinearAlloc" in path;
}

我们从代码可以知道,系统判定为 boot_heap 就是判断路径是否包含boot.art,boot-framework.art,dalvik-LinearAlloc这些 Boot Image

Boot Image 文件

关于 Boot Image 我们可以后续再出一个文章再详细介绍这个地方。

Android 启动时:

  1. Zygote 进程启动
  2. 加载 boot.art → 映射到内存 (boot heap)
  3. 加载 boot-framework.art → 映射到内存 (boot heap)
  4. 预编译的类和方法信息都在这里
  5. ArtMethod 对象也存储在这里 ✅
boot.art:
├─ 预编译的核心 Android 类
├─ 系统库的预编译代码
└─ 启动时加载到内存

boot-framework.art:
├─ Android 框架的预编译类
├─ 系统服务的预编译代码
└─ 启动时加载到内存

/proc/<pid>/maps 输出:

7f1234000000-7f1235000000 rw-p 00000000 /dev/ashmem/dalvik-LinearAlloc
7f1235000000-7f1236000000 rw-p 00000000 /system/framework/boot.art
7f1236000000-7f1237000000 rw-p 00000000 /system/framework/boot-framework.art

ensure_loaded (初始化)

统一初始化入口,确保 Socket 服务器就绪,并发注入多个 Zygote 进程

  • 这里 wait_async 再返回就是避免让程序以为是返回成功了的,我们等待其他实列完成。

  • 创建 Unix Socket 服务器

    • 生成随机名称:/frida-zymbiote-{uuid}
    • 使用抽象命名空间(UnixSocketAddressType.ABSTRACT)
    • 启动监听,等待 Payload 连接
  • 枚举并注入所有 Zygote 进程

    • 查找 zygote、zygote64、usap32、usap64 ( Unspecialized APP Process 也就是Android 的预 fork 进程池机制,用于加速应用启动) 这样尽可能遍历到了所有进程类型,只要有一个新的进程产生就一定会被hook,因为父进程中相关的函数已经被我们修改了。这里的实现非常优雅

      • 传统方式(无USAP):
        应用启动请求 → Zygote fork → 初始化 → 应用运行
                      ↑ 耗时较长
        
        USAP方式:
        Zygote 预fork → USAP池 (空闲进程)
        应用启动请求 → 从USAP池取进程 → 快速配置 → 应用运行
                      ↑ 更快
        
    • 对每个进程调用 do_inject_zygote_agent()

    • 使用并发注入,等待所有完成

private async void ensure_loaded (Cancellable? cancellable) throws Error, IOError {
    while (ensure_request != null) {
        try {
            
            yield ensure_request.future.wait_async (cancellable);
            return;
        } catch (Error e) {
            throw e;
        } catch (IOError e) {
            cancellable.set_error_if_cancelled ();
        }
    }
    ensure_request = new Promise<bool> ();

    if (server_name == null) {
        string name = "/frida-zymbiote-" + Uuid.string_random ().replace ("-", "");
        var address = new UnixSocketAddress.with_type (name, -1, UnixSocketAddressType.ABSTRACT);

        try {
            var socket = new Socket (SocketFamily.UNIX, SocketType.STREAM, SocketProtocol.DEFAULT);
            socket.bind (address, true);
            socket.listen ();

            server_name = name;
            server_address = address;

            handle_zymbiote_connections.begin (socket);
        } catch (GLib.Error raw_err) {
            var err = new Error.TRANSPORT ("%s", raw_err.message);
            ensure_request.reject (err);
            throw err;
        }
    }

    uint pending = 1;
    GLib.Error? first_error = null;

    CompletionNotify on_complete = error => {
        pending--;
        if (error != null && first_error == null)
            first_error = error;

        if (pending == 0) {
            var source = new IdleSource ();
            source.set_callback (ensure_loaded.callback);
            source.attach (MainContext.get_thread_default ());
        }
    };

    foreach (HostProcessInfo info in System.enumerate_processes (new ProcessQueryOptions ())) {
        var name = info.name;
        if (name == "zygote" || name == "zygote64" || name == "usap32" || name == "usap64") {
            uint pid = info.pid;
            if (zymbiote_patches.has_key (pid))
                continue;

            pending++;
            do_inject_zygote_agent.begin (pid, name, cancellable, on_complete);
        }
    }

    on_complete (null);

    yield;

    on_complete = null;

    if (first_error == null) {
        ensure_request.resolve (true);
    } else {
        ensure_request.reject (first_error);
        ensure_request = null;

        throw_api_error (first_error);
    }
}

inject_zymbiote (核心注入逻辑)

我们可以看见 do_inject_zygote_agent 包装了我们的 inject_zymbiote

  • 准备注入数据
    • 调用 prepare_zymbiote_injection() 获取 ZymbiotePrepResult
    • 包含 payload、内存地址、ArtMethod 槽位等
  • 暂停目标进程
    • 发送 SIGSTOP 暂停 Zygote
    • 等待进程停止,确保内存写入安全
  • 应用补丁
    • 创建 ZymbiotePatches 管理补丁
    • 处理已补丁情况:
    • 若 already_patched,从文件读取原始内容
    • 写入 payload 到可执行映射(payload_base)
    • 替换 ArtMethod 槽位指针为 payload 地址
    • 保存补丁记录到 zymbiote_patches[pid]
  • 恢复进程
    • finally 中发送 SIGCONT 恢复执行
private async void do_inject_zygote_agent (uint pid, string name, Cancellable? cancellable, CompletionNotify on_complete) {
    try {
        yield inject_zymbiote (pid, cancellable);

        on_complete (null);
    } catch (GLib.Error e) {
        on_complete (e);
    }
}
private async void inject_zymbiote (uint pid, Cancellable? cancellable) throws Error, IOError {
    // 调用 prepare_zymbiote_injection() 获取 ZymbiotePrepResult
    var prep = yield prepare_zymbiote_injection (pid, cancellable);
    // 发送 SIGSTOP 信号,暂停进程
    Posix.kill ((Posix.pid_t) pid, Posix.Signal.STOP);
    yield wait_until_stopped (pid, cancellable);
    try {
        // 创建 ZymbiotePatches 管理补丁
        var patches = new ZymbiotePatches ();
        // 获取 payload 数据
        unowned uint8[] payload = prep.payload.get_data ();
        // 如果已经补丁,则打开 payload 文件,读取原始数据
        if (prep.already_patched) {
            var handle = Posix.open (prep.payload_path, Posix.O_RDONLY);
            if (handle == -1) {
                throw new Error.PERMISSION_DENIED ("Unable to open payload backing file: %s",
                    strerror (errno));
            }
            var backing_file = new FileDescriptor (handle);
            var original = new uint8[payload.length];
            backing_file.pread_all (original, prep.payload_file_offset);
            //写入 payload 到可执行映射(payload_base)
            patches.apply (payload, prep.process_memory, prep.payload_base, new Bytes.take ((owned) original));
        } else {
            patches.apply (payload, prep.process_memory, prep.payload_base);
        }

        if (prep.already_patched) {
            // 替换 ArtMethod 槽位指针为 payload 地址
            patches.apply (prep.replaced_ptr, prep.process_memory, prep.art_method_slot,
                new Bytes (prep.original_ptr));
        } else {
            patches.apply (prep.replaced_ptr, prep.process_memory, prep.art_method_slot);
        }
        // 保存补丁记录到 zymbiote_patches[pid]
        zymbiote_patches[pid] = patches;
    } finally {
        Posix.kill ((Posix.pid_t) pid, Posix.Signal.CONT);
    }
}

do_prepare_zymbiote_injection(核心准备逻辑)

这里算得上核心注入最核心的部分,也就是解析进程内存映射,解析符号地址,定位 ArtMethod 槽位,准备 Payload 数据。

private async ZymbiotePrepResult prepare_zymbiote_injection (uint pid, Cancellable? cancellable) throws Error, IOError {
    var task = new Task (this, cancellable, (obj, res) => {
        prepare_zymbiote_injection.callback ();
    });
    task.set_task_data ((void *) pid, null);
    task.run_in_thread ((t, source_object, task_data, c) => {
        unowned RoboLauncher launcher = (RoboLauncher) t.get_unowned_source_object ();
        uint pid_to_prep = (uint) task_data;
        try {
            var r = do_prepare_zymbiote_injection (pid_to_prep, launcher.server_name);
            t.return_pointer ((owned) r, Object.unref);
        } catch (GLib.Error e) {
            t.return_error ((owned) e);
        }
    });
    yield;

    try {
        return (ZymbiotePrepResult) (owned) task.propagate_pointer ();
    } catch (GLib.Error e) {
        throw_api_error (e);
    }
}
private static ZymbiotePrepResult do_prepare_zymbiote_injection (uint pid, string server_name) throws Error, IOError {
    // 进行一些变量的初始化
    uint64 payload_base = 0;
    string? payload_path = null;
    uint64 payload_file_offset = 0;
    string? libc_path = null;
    string? runtime_path = null;
    Gee.List<Gum.MemoryRange?> heap_candidates = new Gee.ArrayList<Gum.MemoryRange?> ();

    var iter = ProcMapsIter.for_pid (pid);
    while (iter.next ()) {
        string path = iter.path;
        string flags = iter.flags;
        if (path.has_suffix ("/libstagefright.so") && "x" in flags) {
            if (payload_base == 0) {
                payload_base = iter.end_address - Gum.query_page_size ();
                payload_path = path;
                payload_file_offset = iter.file_offset;
            }
        } else if (path.has_suffix ("/libc.so")) {
            if (libc_path == null)
                libc_path = path;
        } else if (path.has_suffix ("/libandroid_runtime.so")) {
            if (runtime_path == null)
                runtime_path = path;
        } else if (flags == "rw-p" && is_boot_heap (path)) {
            uint64 start = iter.start_address;
            uint64 end = iter.end_address;
            heap_candidates.add (Gum.MemoryRange () {
                base_address = start,
                size = (size_t) (end - start),
            });
        }
    }

    if (payload_base == 0)
        throw new Error.NOT_SUPPORTED ("Unable to pick a payload base");
    if (libc_path == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libc.so path");
    if (runtime_path == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libandroid_runtime.so path");
    if (heap_candidates.is_empty)
        throw new Error.NOT_SUPPORTED ("Unable to detect any VM heap candidates");

    var libc_entry = ProcMapsSoEntry.find_by_path (pid, libc_path);
    if (libc_entry == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libc.so entry");

    var runtime_entry = ProcMapsSoEntry.find_by_path (pid, runtime_path);
    if (runtime_entry == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libandroid_runtime.so entry");

    Gum.ElfModule libc;
    try {
        libc = new Gum.ElfModule.from_file (libc_path);
    } catch (Gum.Error e) {
        throw new Error.NOT_SUPPORTED ("Unable to parse libc.so: %s", e.message);
    }

    Gum.ElfModule runtime;
    try {
        runtime = new Gum.ElfModule.from_file (runtime_path);
    } catch (Gum.Error e) {
        throw new Error.NOT_SUPPORTED ("Unable to parse libandroid_runtime.so: %s", e.message);
    }

    uint64 set_argv0_address = 0;
    runtime.enumerate_exports (e => {
        if (e.name == "_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring") {
            set_argv0_address = runtime_entry.base_address + e.address;
            return false;
        }
        return true;
    });
    if (set_argv0_address == 0)
        throw new Error.NOT_SUPPORTED ("Unable to locate android.os.Process.setArgV0(); please file a bug");

    uint pointer_size = ("/lib64/" in libc_path) ? 8 : 4;

    var original_ptr = new uint8[pointer_size];
    var replaced_ptr = new uint8[pointer_size];

    (new Buffer (new Bytes.static (original_ptr), ByteOrder.HOST, pointer_size)).write_pointer (0, set_argv0_address);
    (new Buffer (new Bytes.static (replaced_ptr), ByteOrder.HOST, pointer_size)).write_pointer (0, payload_base);

    var fd = open_process_memory (pid);

    uint64 art_method_slot = 0;
    bool already_patched = false;
    foreach (var candidate in heap_candidates) {
        var heap = new uint8[candidate.size];
        var n = fd.pread (heap, candidate.base_address);
        if (n != heap.length)
            throw new Error.NOT_SUPPORTED ("Short read");

        void * p = memmem (heap, original_ptr);
        if (p == null) {
            p = memmem (heap, replaced_ptr);
            already_patched = p != null;
        }

        if (p != null) {
            art_method_slot = candidate.base_address + ((uint8 *) p - (uint8 *) heap);
            break;
        }
    }
    if (art_method_slot == 0)
        throw new Error.NOT_SUPPORTED ("Unable to locate method slot; please file a bug");

    var blob = (pointer_size == 8)
#if ARM || ARM64
        ? Frida.Data.Android.get_zymbiote_arm64_bin_blob ()
        : Frida.Data.Android.get_zymbiote_arm_bin_blob ();
#else
        ? Frida.Data.Android.get_zymbiote_x86_64_bin_blob ()
        : Frida.Data.Android.get_zymbiote_x86_bin_blob ();
#endif

    unowned uint8[] payload_template = blob.data;
    void * p = memmem (payload_template, "/frida-zymbiote-00000000000000000000000000000000".data);
    assert (p != null);
    size_t data_offset = (uint8 *) p - (uint8 *) payload_template;

    var payload = new Buffer (new Bytes (payload_template), ByteOrder.HOST, pointer_size);

    size_t cursor = data_offset;
    payload.write_string (cursor, server_name);
    cursor += 64;

    payload.write_pointer (cursor, art_method_slot);
    cursor += pointer_size;

    payload.write_pointer (cursor, set_argv0_address);
    cursor += pointer_size;

    string[] wanted = {
        "socket",
        "connect",
        "__errno",
        "getpid",
        "getppid",
        "sendmsg",
        "recv",
        "close",
        "raise",
    };

    var index_of = new Gee.HashMap<string, int> ();
    for (int i = 0; i != wanted.length; i++)
        index_of[wanted[i]] = i;

    var addrs = new uint64[wanted.length];
    uint pending = wanted.length;
    libc.enumerate_exports (e => {
        if (index_of.has_key (e.name)) {
            int idx = index_of[e.name];
            addrs[idx] = libc_entry.base_address + e.address;
            pending--;
        }
        return pending != 0;
    });

    for (int i = 0; i != addrs.length; i++) {
        assert (addrs[i] != 0);
        payload.write_pointer (cursor, addrs[i]);
        cursor += pointer_size;
    }

    return new ZymbiotePrepResult () {
        process_memory = fd,
        already_patched = already_patched,

        art_method_slot = art_method_slot,
        original_ptr = original_ptr,
        replaced_ptr = replaced_ptr,

        payload = payload.bytes,
        payload_base = payload_base,
        payload_path = payload_path,
        payload_file_offset = payload_file_offset,
    };
}

调用链

do_prepare_zymbiote_injection() (linux-host-session.vala:925)
          ├─ ProcMapsIter.for_pid() - 创建内存映射迭代器
          ├─ ProcMapsIter.next() - 遍历内存映射
          ├─ ProcMapsSoEntry.find_by_path() - 查找 SO 映射条目
          ├─ Gum.ElfModule.from_file() - 解析 ELF 文件
          ├─ Gum.ElfModule.enumerate_exports() - 枚举导出符号
          ├─ open_process_memory() - 打开进程内存 (linux-host-session.vala:1692)
          │   └─ Posix.open("/proc/<pid>/mem") - 打开 /proc/<pid>/mem
          ├─ FileDescriptor.pread() - 读取进程内存
          ├─ memmem() - 内存搜索 (linux-host-session.vala:1110)
          ├─ is_boot_heap() - 判断是否为 boot heap (linux-host-session.vala:1107)
          └─ Buffer.write_pointer() / write_string() - 填充 payload

初始化变量

            uint64 payload_base = 0;
            string? payload_path = null;
            uint64 payload_file_offset = 0;
            string? libc_path = null;
            string? runtime_path = null;
            Gee.List<Gum.MemoryRange?> heap_candidates = new Gee.ArrayList<Gum.MemoryRange?> ();
  • payload_base: payload 注入地址
  • payload_path: payload 所在文件路径
  • payload_file_offset: 文件偏移(用于已补丁情况)
  • libc_path: libc.so 路径
  • runtime_path: libandroid_runtime.so 路径
  • heap_candidates: boot heap 候选区域列表

解析进程内存映射

  • 创建内存映射迭代器
    • ProcMapsIter.for_pid(pid) 读取 /proc/<pid>/maps
    • 逐行解析内存映射
  • 定位 payload 基址(17.6.1 改进)
    • 查找 libstagefright.so 且可执行("x" in flags)
    • 选择映射末页:end_address - page_size
    • 记录文件路径和偏移,用于已补丁情况
  • 定位 libc.so与 libandroid_runtime.so
    • 记录第一个匹配的 libc.so 路径
    • 记录第一个匹配的 libandroid_runtime.so 路径
  • 收集 boot heap 候选
    • 检查 flags == "rw-p" 且通过 is_boot_heap() 判断
    • is_boot_heap() 检查路径是否包含:
    • "boot.art"
    • "boot-framework.art"
    • "dalvik-LinearAlloc"
    • 添加到 heap_candidates
var iter = ProcMapsIter.for_pid (pid);
while (iter.next ()) {
    string path = iter.path;
    string flags = iter.flags;
    if (path.has_suffix ("/libstagefright.so") && "x" in flags) {
            if (payload_base == 0) {
                    payload_base = iter.end_address - Gum.query_page_size ();
                    payload_path = path;
                    payload_file_offset = iter.file_offset;
            }
    } else if (path.has_suffix ("/libc.so")) {
            if (libc_path == null)
                    libc_path = path;
    } else if (path.has_suffix ("/libandroid_runtime.so")) {
            if (runtime_path == null)
                    runtime_path = path;
    } else if (flags == "rw-p" && is_boot_heap (path)) {
            uint64 start = iter.start_address;
            uint64 end = iter.end_address;
            heap_candidates.add (Gum.MemoryRange () {
                    base_address = start,
                    size = (size_t) (end - start),
            });
    }
}

获取 libc 与 runtime 信息

其实就是校验我们必须需要的信息是否存在,然后再根据我们的 libc_path 与 runtime_path 找到 libc 与 runtime 的基地址。然后对应进行符号解析得到 ELF 对象,然后查找我们的 setArgV0Native 函数(_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring)。

为什么需要查找我们的 setArgV0Native 函数捏?因为这是后续查找ArtMethod的关键,artMethod 的函数指针就是存储的 setArgV0Native 函数地址,所以我们可以通过解析符号表定位到 setArgV0Native 函数就可以间接定位到 ArtMethod

if (payload_base == 0)
        throw new Error.NOT_SUPPORTED ("Unable to pick a payload base");
if (libc_path == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libc.so path");
if (runtime_path == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libandroid_runtime.so path");
if (heap_candidates.is_empty)
        throw new Error.NOT_SUPPORTED ("Unable to detect any VM heap candidates");

var libc_entry = ProcMapsSoEntry.find_by_path (pid, libc_path);
if (libc_entry == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libc.so entry");

var runtime_entry = ProcMapsSoEntry.find_by_path (pid, runtime_path);
if (runtime_entry == null)
        throw new Error.NOT_SUPPORTED ("Unable to detect libandroid_runtime.so entry");
        
Gum.ElfModule libc;
try {
        libc = new Gum.ElfModule.from_file (libc_path);
} catch (Gum.Error e) {
        throw new Error.NOT_SUPPORTED ("Unable to parse libc.so: %s", e.message);
}

Gum.ElfModule runtime;
try {
        runtime = new Gum.ElfModule.from_file (runtime_path);
} catch (Gum.Error e) {
        throw new Error.NOT_SUPPORTED ("Unable to parse libandroid_runtime.so: %s", e.message);
}

uint64 set_argv0_address = 0;
runtime.enumerate_exports (e => {
        if (e.name == "_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring") {
                set_argv0_address = runtime_entry.base_address + e.address;
                return false;
        }
        return true;
});
if (set_argv0_address == 0)
        throw new Error.NOT_SUPPORTED ("Unable to locate android.os.Process.setArgV0(); please file a bug");

定位 ArtMethod 信息

根据前面获取的 setArgV0Native 函数指针在 boot_heap 区域搜索就可以找到我们的 ArtMethod

var fd = open_process_memory (pid);

uint64 art_method_slot = 0;
bool already_patched = false;
foreach (var candidate in heap_candidates) {
        var heap = new uint8[candidate.size];
        var n = fd.pread (heap, candidate.base_address);
        if (n != heap.length)
                throw new Error.NOT_SUPPORTED ("Short read");

        void * p = memmem (heap, original_ptr);
        if (p == null) {
                p = memmem (heap, replaced_ptr);
                already_patched = p != null;
        }

        if (p != null) {
                art_method_slot = candidate.base_address + ((uint8 *) p - (uint8 *) heap);
                break;
        }
}
if (art_method_slot == 0)
        throw new Error.NOT_SUPPORTED ("Unable to locate method slot; please file a bug");

填充payload

这里搜索frida-zymbiote-00000000000000000000000000000000,是因为在 zymbiote 中初始化name就是这个名字

static volatile const FridaApi frida =

{

.name = "/frida-zymbiote-00000000000000000000000000000000",

};

只要找到这个位置,那么就根据这个位置后面填写我们对应的数据即可,完善这个 FridaApi 结构体

var blob = (pointer_size == 8)
#if ARM || ARM64
        ? Frida.Data.Android.get_zymbiote_arm64_bin_blob ()
        : Frida.Data.Android.get_zymbiote_arm_bin_blob ();
#else
        ? Frida.Data.Android.get_zymbiote_x86_64_bin_blob ()
        : Frida.Data.Android.get_zymbiote_x86_bin_blob ();
#endif

unowned uint8[] payload_template = blob.data;
void * p = memmem (payload_template, "/frida-zymbiote-00000000000000000000000000000000".data);
assert (p != null);
size_t data_offset = (uint8 *) p - (uint8 *) payload_template;

var payload = new Buffer (new Bytes (payload_template), ByteOrder.HOST, pointer_size);

size_t cursor = data_offset;
payload.write_string (cursor, server_name);
cursor += 64;

payload.write_pointer (cursor, art_method_slot);
cursor += pointer_size;

payload.write_pointer (cursor, set_argv0_address);
cursor += pointer_size;

string[] wanted = {
        "socket",
        "connect",
        "__errno",
        "getpid",
        "getppid",
        "sendmsg",
        "recv",
        "close",
        "raise",
};

var index_of = new Gee.HashMap<string, int> ();
for (int i = 0; i != wanted.length; i++)
        index_of[wanted[i]] = i;

var addrs = new uint64[wanted.length];
uint pending = wanted.length;
libc.enumerate_exports (e => {
        if (index_of.has_key (e.name)) {
                int idx = index_of[e.name];
                addrs[idx] = libc_entry.base_address + e.address;
                pending--;
        }
        return pending != 0;
});

for (int i = 0; i != addrs.length; i++) {
        assert (addrs[i] != 0);
        payload.write_pointer (cursor, addrs[i]);
        cursor += pointer_size;
}
  • 我们可以知道 payload 的结构体

其实就是将Payload需要的libc函数地址,ArtMethod函数地址填充进去,方便后续运作

  1. name[64] - Unix socket 名称
  2. art_method_slot - ArtMethod 槽位指针地址
  3. original_set_argv0 - 原始函数指针
  4. socket - libc socket 函数
  5. connect - libc connect 函数
  6. __errno - libc errno 函数
  7. getpid - libc getpid 函数
  8. getppid - libc getppid 函数
  9. sendmsg - libc sendmsg 函数
  10. recv - libc recv 函数
  11. close - libc close 函数
  12. raise - libc raise 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct _FridaApi
{
  char name[64];
 
  void    ** art_method_slot;
  void    (* original_set_argv0) (JNIEnv * env, jobject clazz, jstring name);
 
  int     (* socket) (int domain, int type, int protocol);
  int     (* connect) (int sockfd, const struct sockaddr * addr, socklen_t addrlen);
  int *   (* __errno) (void);
  pid_t   (* getpid) (void);
  pid_t   (* getppid) (void);
  ssize_t (* sendmsg) (int sockfd, const struct msghdr * msg, int flags);
  ssize_t (* recv) (int sockfd, void * buf, size_t len, int flags);
  int     (* close) (int fd);
  int     (* raise) (int sig);
};

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 5
支持
分享
最新回复 (3)
雪    币: 104
活跃值: (7531)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
1天前
0
雪    币: 3656
活跃值: (3386)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
tql
8小时前
0
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
牛逼
4小时前
0
游客
登录 | 注册 方可回帖
返回