iOS逆向工程:frida-trace 详细使用教程

2,088 阅读7分钟

1. 简介

什么是 Frida

Frida 是一款基于 Python 和 JavaScript 的动态代码插桩工具,能够在运行时注入代码到原生应用程序中。它支持 iOS、Android、macOS、Windows、Linux 等多个平台,常用于安全研究、逆向工程和应用分析。在 iOS 平台上,Frida 特别适合用于分析无源码应用和系统框架。

什么是 frida-trace

frida-trace 是 Frida 提供的一个命令行工具,用于快速跟踪函数调用。它可以自动生成用于跟踪函数调用的 JavaScript 钩子脚本,非常适合初步分析 iOS 应用程序的行为。对于 Objective-C 方法,frida-trace 可以轻松跟踪类和方法的调用过程。

主要特点

  • 跨平台支持,对 iOS 有良好适配
  • 实时函数跟踪
  • 自动生成钩子脚本
  • 可以修改函数参数和返回值
  • 支持本地连接 USB 设备和远程设备

2. 安装与配置

环境要求

  • Python 3.x
  • pip(Python 包管理器)
  • iOS 设备(有以下两种使用方式):
    • 越狱设备:可直接注入 Frida
    • 非越狱设备:需使用开发者证书重签名应用,或使用 Frida-Gadget

注意:本文主要介绍在越狱设备下的使用。

安装步骤

  1. 安装 Frida
pip install frida-tools
  1. iOS 设备配置

在 Sileo App上 添加 Frida 源并安装:

# 在 Sileo 中添加源
https://build.frida.re

# 安装 Frida 包

验证安装

# 检查 Frida 版本
frida --version

# 列出连接的设备
frida-ls-devices

# 列出正在运行的进程
frida-ps -U  # 针对 USB 连接的设备
frida-ps -R  # 针对远程设备

3. 基础入门

基本概念

  • 目标进程:要跟踪的 iOS 应用程序
  • 钩子(Hook):插入到目标进程中的代码
  • 处理程序(Handler):定义钩子行为的 JavaScript 函数

基本命令格式

frida-trace [选项] -[操作] <目标>

常见选项:

  • -U:USB 连接的 iOS 设备
  • -R:远程设备
  • -f:启动应用
  • -p:附加到进程 ID
  • -n:按名称附加到进程

常见操作:

  • -i:包含要跟踪的函数
  • -m:使用模式匹配跟踪函数,特别适合 Objective-C 方法
  • -a:添加模块
  • -I:包含/排除导入函数

基本用法示例

  1. 跟踪特定函数
# 跟踪 open 系统调用
frida-trace -U -i "open" 应用名

# 跟踪多个函数
frida-trace -U -i "open" -i "close" 应用名
  1. 使用模式匹配跟踪 Objective-C 方法
# 跟踪UIViewController类下的所有 实例方法
frida-trace -U -m "-[UIViewController *]" 应用名

# 跟踪UIViewController类下的所有 类方法
frida-trace -U -m "+[UIViewController *]" 应用名

# 使用通配符*,来跟踪UIViewController所有的实例方法和类方法
frida-trace -U -m "*[UIViewController *]" 应用名

# 跟踪指定方法
frida-trace -U -m "-[UIViewController viewDidLoad]" 应用名

# 跟踪多个类的方法
frida-trace -U -m "-[UIViewController *]" -m "-[UINavigationController *]" 应用名
  1. 跟踪框架中的方法
# 跟踪 UIKit 框架中的方法
frida-trace -U -m "-[UI* *]" 应用名

# 跟踪 Foundation 框架中的方法
frida-trace -U -m "-[NS* *]" 应用名
  1. 根据特定动作跟踪
# 跟踪 URL 加载相关方法
frida-trace -U -m "*[* URL*]" 应用名

# 跟踪网络请求相关方法
frida-trace -U -m "*[NSURLSession *]" -m "*[NSURLConnection *]" 应用名

4. 进阶使用

自定义 JavaScript 处理程序

frida-trace 会在当前目录生成名为 __handlers__ 的文件夹,其中包含为每个跟踪的函数生成的 js 文件。你可以编辑这些文件来自定义行为。

基本结构:

// 文件名: 函数名_函数地址.js
defineHandler({
  onEnter(log, args, state) {
    log("函数被调用,参数:" + args[0]);
    // 保存状态以便在 onLeave 中使用
    state.args0 = args[0];
  },

  onLeave(log, retval, state) {
    log("函数返回,返回值:" + retval);
    log("之前保存的参数:" + state.args0);

    // 可以修改返回值
    // retval.replace(0);
  },
});

获取和修改参数值

对于不同类型的参数,Frida 提供了不同的访问方式:

整数和指针:

onEnter(log, args, state) {
  // 读取整数参数
  log('参数值: ' + args[0].toInt32());

  // 对于64位整数
  log('64位参数: ' + args[0].toString());
}

字符串:

onEnter(log, args, state) {
  // 读取字符串参数
  log('字符串: ' + args[0].readUtf8String());

  // 或者读取特定长度
  log('固定长度字符串: ' + args[0].readUtf8String(10));
}

Objective-C 对象:

onEnter(log, args, state) {
  // 将参数转换为 ObjC 对象
  const objcObj = new ObjC.Object(args[0]);

  // 读取属性或调用方法
  log('对象描述: ' + objcObj.toString());
  log('对象类: ' + objcObj.$className);

  // 调用方法
  log('属性值: ' + objcObj.propertyName());

  // 访问 NSString
  if (objcObj.$className === 'NSString') {
    log('字符串值: ' + objcObj.toString());
  }

  // 访问 NSArray
  if (objcObj.$className === 'NSArray' || objcObj.$className.indexOf('NSArray') !== -1) {
    const count = objcObj.count().valueOf();
    log('数组元素数量: ' + count);
    for (let i = 0; i < count; i++) {
      log(`数组元素[${i}]: ${objcObj.objectAtIndex_(i)}`);
    }
  }

  // 访问 NSDictionary
  if (objcObj.$className === 'NSDictionary' || objcObj.$className.indexOf('NSDictionary') !== -1) {
    const keys = objcObj.allKeys();
    const count = keys.count().valueOf();
    log('字典元素数量: ' + count);
    for (let i = 0; i < count; i++) {
      const key = keys.objectAtIndex_(i);
      const value = objcObj.objectForKey_(key);
      log(`字典元素[${key}]: ${value}`);
    }
  }
}

结构体和复杂对象:

onEnter(log, args, state) {
  // 读取结构体字段
  log('结构体字段1: ' + args[0].add(0).readPointer().toInt32());
  log('结构体字段2: ' + args[0].add(4).readPointer().toInt32());
}

修改参数和返回值

onEnter(log, args, state) {
  // 修改参数
  args[0] = ptr('0x12345678');

  // 对于字符串
  const newString = Memory.allocUtf8String('新字符串');
  args[0] = newString;

  // 对于 Objective-C 对象
  // 例如修改 NSString 参数
  const NSString = ObjC.classes.NSString;
  args[1] = NSString.stringWithString_("修改后的字符串");
}

onLeave(log, retval, state) {
  // 修改返回值
  retval.replace(1);  // 改为返回 1

  // 如果返回值是 ObjC 对象,也可以替换
  const NSString = ObjC.classes.NSString;
  retval.replace(NSString.stringWithString_("修改后的返回值"));
}

添加额外跟踪点

在运行时,你可以在现有处理程序中添加额外的跟踪点:

onEnter(log, args, state) {
  // 找到函数地址
  const targetAddr = Module.findExportByName(null, 'open');

  // 为其添加拦截
  Interceptor.attach(targetAddr, {
    onEnter(args) {
      log('open called with: ' + args[0].readUtf8String());
    }
  });

  // 拦截 Objective-C 方法
  const UIApplication = ObjC.classes.UIApplication;
  Interceptor.attach(UIApplication['- openURL:'].implementation, {
    onEnter(args) {
      const url = new ObjC.Object(args[2]);
      log('打开 URL: ' + url.toString());
    }
  });
}

访问内存和模块

// 查找模块
const module = Process.findModuleByName("UIKit");
log("模块基址: " + module.base);

// 查找导出函数
const func = Module.findExportByName(null, "UIApplicationMain");
log("函数地址: " + func);

// 读取内存
const data = Memory.readByteArray(ptr("0x12345678"), 32);
log("内存数据: " + hexdump(data));

枚举 Objective-C 类和方法

// 获取所有已加载的类
const classes = ObjC.classes;
for (const className in classes) {
  log("类名: " + className);
}

// 获取特定类的所有方法
const UIViewController = ObjC.classes.UIViewController;
const methods = UIViewController.$ownMethods;
for (const method of methods) {
  log("方法: " + method);
}

// 获取类的所有属性
const properties = UIViewController.$ownProperties;
for (const property in properties) {
  log("属性: " + property);
}

5. 实战案例

案例 1: 跟踪网络请求

下面的示例展示如何跟踪 iOS 应用的网络请求:

frida-trace -U -m "-[NSURLSession dataTaskWith*]" 应用名

然后编辑生成的处理程序,如 __handlers__/NSURLSession/dataTaskWithURL_.js

defineHandler({
  onEnter(log, args, state) {
    const session = new ObjC.Object(args[0]);
    const url = new ObjC.Object(args[2]);
    log(`[+] 发现网络请求: ${url.toString()}`);

    state.url = url.toString();

    // 可以打印请求头
    if (args[2].$className === "NSMutableURLRequest") {
      const request = args[2];
      const headers = request.allHTTPHeaderFields();
      if (headers) {
        const keys = headers.allKeys();
        const count = keys.count().valueOf();
        log("HTTP Headers:");
        for (let i = 0; i < count; i++) {
          const key = keys.objectAtIndex_(i);
          const value = headers.objectForKey_(key);
          log(`  ${key}: ${value}`);
        }
      }

      // 打印 HTTP 方法
      log(`HTTP Method: ${request.HTTPMethod()}`);

      // 打印请求体
      const body = request.HTTPBody();
      if (body) {
        const length = body.length().valueOf();
        if (length > 0) {
          const data = Memory.readByteArray(body.bytes(), length);
          log(`Request Body (${length} bytes):`);
          log(hexdump(data, { header: false, ansi: false }));

          // 尝试解析为字符串
          try {
            const bodyStr = body.toString();
            log(`Body as string: ${bodyStr}`);
          } catch (e) {
            log("Body cannot be converted to string");
          }
        }
      }
    }
  },

  onLeave(log, retval, state) {
    log(`[-] 完成请求: ${state.url}`);
    // 可以在这里获取响应(需要额外的钩子)
  },
});

案例 2: 跟踪用户界面事件

frida-trace -U -m "-[UIControl sendAction:to:forEvent:]" 应用名

编辑生成的处理程序:

defineHandler({
  onEnter(log, args, state) {
    const control = new ObjC.Object(args[0]);
    const action = args[2];
    const target = new ObjC.Object(args[3]);
    const event = new ObjC.Object(args[4]);

    log(`[+] UI 事件被触发`);
    log(`    控件类型: ${control.$className}`);
    log(`    动作: ${ObjC.selectorAsString(action)}`);
    log(`    目标: ${target.$className}`);

    // 获取控件的标题(如按钮文本)
    if (control.$className === "UIButton") {
      const title = control.titleForState_(0); // UIControlStateNormal
      if (title) {
        log(`    按钮标题: ${title}`);
      }
    }

    // 保存状态
    state.action = ObjC.selectorAsString(action);
    state.target = target.$className;
  },

  onLeave(log, retval, state) {
    log(`[-] 完成事件: ${state.action} on ${state.target}`);
  },
});

案例 3: 绕过 SSL 证书锁定

以下示例展示如何绕过 iOS 应用的 SSL 证书锁定:

frida-trace -U -n 应用名 -m "*[NSURLSession *]" -m "*[NSURLConnection *]"

然后创建自定义脚本 ssl_bypass.js

defineHandler({
  onLoad() {
    // 替换 SecTrustEvaluate
    Interceptor.replace(
      Module.findExportByName("Security", "SecTrustEvaluate"),
      new NativeCallback(
        (trust, result) => {
          log("[+] SecTrustEvaluate 被调用");
          Memory.writePointer(result, ptr("0x1")); // 始终返回成功
          return 0; // errSecSuccess
        },
        "int",
        ["pointer", "pointer"]
      )
    );

    // 处理 NSURLSession 的验证
    const SSLHandshake = ObjC.classes.NSURLSession.constructor.constructor;
    const originalValidation =
      SSLHandshake["- URLSession:didReceiveChallenge:completionHandler:"];

    SSLHandshake["- URLSession:didReceiveChallenge:completionHandler:"] =
      ObjC.implement(
        SSLHandshake["- URLSession:didReceiveChallenge:completionHandler:"],
        function (handle, selector, session, challenge, completionHandler) {
          log("[+] NSURLSession 证书验证被拦截");

          // 获取证书挑战
          const objcChallenge = new ObjC.Object(challenge);
          log(
            `    挑战类型: ${objcChallenge
              .protectionSpace()
              .authenticationMethod()}`
          );

          // 始终接受证书
          const kCFStreamSSLValidateServerCertificate = 0;
          const credential = ObjC.classes.NSURLCredential.credentialForTrust_(
            objcChallenge.protectionSpace().serverTrust()
          );
          const completionHandlerBlock = new ObjC.Block(completionHandler);
          completionHandlerBlock.implementation(
            kCFStreamSSLValidateServerCertificate, // NSURLSessionAuthChallengeUseCredential
            credential
          );
        }
      );
  },
});

案例 4: 跟踪 Keychain 访问

frida-trace -U -n 应用名 -i "SecItemAdd" -i "SecItemCopyMatching" -i "SecItemUpdate" -i "SecItemDelete"

编辑生成的处理程序,例如 __handlers__/Security/SecItemAdd.js

defineHandler({
  onEnter(log, args, state) {
    log(`[+] SecItemAdd 被调用 - 尝试写入 Keychain`);

    // 获取第一个参数(CFDictionaryRef)
    const dict = new ObjC.Object(args[0]);

    // 打印 Keychain 项目内容
    const keys = dict.allKeys();
    const count = keys.count().valueOf();

    log(`Keychain 项目 (${count} 个属性):`);
    for (let i = 0; i < count; i++) {
      const key = keys.objectAtIndex_(i);
      let value = dict.objectForKey_(key);

      // 特殊处理密码字段
      if (key.toString() === "v_Data") {
        try {
          // 尝试解码为字符串
          const rawData = Memory.readByteArray(
            value.bytes(),
            value.length().valueOf()
          );
          log(`  ${key} = [二进制数据 ${value.length().valueOf()} 字节]`);
          log(`    内容: ${hexdump(rawData, { header: false, ansi: false })}`);

          // 尝试转换为字符串
          value = ObjC.classes.NSString.alloc().initWithData_encoding_(
            value,
            4
          ); // NSUTF8StringEncoding
          log(`    UTF8字符串: ${value}`);
        } catch (e) {
          log(`  ${key} = [无法解码的二进制数据]`);
        }
      } else {
        log(`  ${key} = ${value}`);
      }
    }

    state.dict = dict;
  },

  onLeave(log, retval, state) {
    log(`[-] SecItemAdd 返回: ${retval.toInt32()}`);
    // OSStatus 返回码 (0 表示成功)
    if (retval.toInt32() === 0) {
      log(`    Keychain 项目添加成功`);
    } else {
      log(`    Keychain 项目添加失败,错误码: ${retval.toInt32()}`);
    }
  },
});

6. 资源与参考资料

官方资源

社区资源

iOS 特定资源

推荐工具

  • iOS 特定工具
    • Frida-iOS-Dump - 基于 Frida 的 iOS 应用砸壳工具
    • bagbak - iOS 应用砸壳工具,支持 App 和 Extension
    • bfinject - 向运行中的 iOS 应用注入动态库
    • SSL Kill Switch 2 - 禁用 iOS 应用的 SSL 证书验证
  • 配套工具