你是否想过窥探 iPhone 或 iPad 上的应用,观察它到底在处理什么数据?今天介绍一种简单而强大的方法——使用 Frida 动态获取 iOS 应用中 Block 的参数类型。

什么是 Block?

在 iOS 编程中,Block 是一小段可以被传递、稍后执行的自包含代码。可以把它想象成你把电话号码(Block)给朋友,朋友在有重要事情时给你回电话。

有时你想精确观察这些 Block 接收了什么输入、产出了什么结果。Frida 可以在不修改应用原始代码的情况下帮你做到这一点。

Block 的内存结构

理解 Block 的内部结构有助于你准确地获取它的签名和参数信息。以下是 Block 的内存布局:

struct Block_literal_1 {
    void *isa; // 指向 &_NSConcreteStackBlock 或 &_NSConcreteGlobalBlock
    int flags; // 标志位,指示 block 类型和属性
    int reserved; // 保留字段
    void (*invoke)(struct Block_literal_1 *, ...); // 指向 block 实现的函数指针
    struct Block_descriptor_1 {
        unsigned long reserved;     // 保留,通常为 NULL
        unsigned long size;         // Block_literal_1 结构体的大小
        // 可选的辅助函数
        void (*copy_helper)(void *dst, void *src);     // flag (1<<25) 置位时存在
        void (*dispose_helper)(void *src);             // flag (1<<25) 置位时存在
        // 包含签名的 block 必须有此字段
        const char *signature;                         // flag (1<<30) 置位时存在
    } *descriptor;
    // 捕获的变量(如果有)
};

Block 内部的 signature 提供了返回类型和参数类型等关键信息,是动态分析的基础。

什么是 Frida?

“将你的脚本注入黑盒进程。Hook 函数、监控加密 API、追踪私有应用逻辑——无需源码。”

Frida 是一个强大的动态 instrumentation 工具,让你在运行时观察和操控应用。可以把 Frida 想象成一个魔法显微镜,让你实时看到应用内部发生的一切。

用 Frida 发现 Block 的参数

下面以一个 iOS 应用为例,逐步展示如何动态获取 Block 参数。

第 1 步:将 Frida 连接到应用

将设备连接到电脑,在终端运行:

frida -U -n Safari

这会将 Frida attach 到 iPhone 上的 Safari。

第 2 步:动态打印 Block

假设我们想观察 MMNetworkManager 中名为 addSecurityCheckBlock 的 Block。动态获取并打印:

const blk =
  ObjC.classes.MMNetworkManager.sharedInstance().addSecurityCheckBlock();
console.log("Block Address:", blk);

这会输出 Block 的内存地址。

第 3 步:解读 Block 签名

Block 有一个内部签名(signature),描述其输入和输出。以下是动态打印签名的方法:

const block = new ObjC.Block(blk);
const signaturePtr = block.handle
  .add(0x18)
  .readPointer() // descriptor 指针
  .add(0x10)
  .readPointer(); // signature 指针

const signature = signaturePtr.readCString();
console.log("Signature:", signature);

典型的签名如下:

Signature: @"NSDictionary"24@?0@"NSURL"8@"NSDictionary"16

其中的数字是调用 Block 时栈帧中的字节偏移:

签名部分 含义 偏移(字节)
@"NSDictionary"返回类型 返回 NSDictionary * 总计 24 字节
@?0 Block 自身(@?),隐藏的第一个参数 0 字节
@"NSURL"8 第一个可见参数:NSURL * 8 字节
@"NSDictionary"16 第二个可见参数:NSDictionary * 16 字节

第 4 步:清晰地解码签名

用以下脚本简化签名解码:

const sigClean = signature.replace(/\d+/g, ""); // 去掉数字
const types = sigClean.match(/@\?|@".*?"|[@#:vBcCiIlLqQfd]/g);

function decodeType(type) {
  if (type === "@?") return "block";
  if (type.startsWith('@"')) return type.slice(2, -1) + " *";
  switch (type) {
    case "v":
      return "void";
    case "@":
      return "id";
    case "#":
      return "Class";
    default:
      return "unknown";
  }
}

console.log("Return →", decodeType(types[0]));
for (let i = 2; i < types.length; i++) {
  console.log("Arg", i - 1, "→", decodeType(types[i]));
}

输出结果清晰可读:

Return → NSDictionary *
Arg 1 → NSURL *
Arg 2 → NSDictionary *

总结

通过 Frida,你可以动态地探索和理解 ObjC 应用中传递给 Block 的参数。清楚理解 Block 的内存结构、签名和偏移后,你就能在不修改代码的情况下分析任何应用的内部逻辑。

参考