iOS逆向 - 运行时分析(三)Frida

7,344 阅读9分钟

欢迎关注微信公众号:FSA全栈行动 👋

一、简介

Frida 是一个跨平台的轻量级 Hook 框架,支持 MacOSLinuxWindows 操作系统,提供了精简的 Python 接口和功能丰富的 JS 接口,除了可以使用自身的控制台交互以外,还可以使用 PythonJS 脚本注入到运行程序中,通过 Frida 可以获取程序的详细信息、拦截和调用指定函数、注入代码、修改参数等。

Frida 源代码托管:github.com/frida

二、安装

iOS

添加软件源 https://build.frida.re/,然后搜索 Frida 安装即可。

安装完成后可能通过 ps 看到 frida-server 后台程序则说明安装成功,若没有可以重启手机后再看看

lxf-iPad:~ root# ps -ax | grep frida
26717 ??         0:00.08 /usr/sbin/frida-server
26731 ttys000    0:00.01 grep frida

MacOS

使用 pip 进行安装

pip install frida-tools # CLI tools
pip install frida       # Python bindings

后续需要升级的话,可以使用 --upgrade 参数

pip install frida-tools --upgrade
pip install frida --upgrade

如果报 command not found:pip 错误,说明当前系统没有安装 pip,可以使用下方命令安装

brew install wget
wget https://bootstrap.pypa.io/get-pip.py
python3 get-pip.py

执行完成后会提示你将对应 python 版本的 bin 路径添加到 PATH 中,如:

export PATH=~/Library/Python/3.8/bin:$PATH

当然,如果你也有用 pyenv,则可以忽略上述的 pip 安装流程,因为 pyenv 自带了 pip

你可以用 which 命令查看你的 pip 的安装位置。

➜  ~ which pip
/usr/local/var/pyenv/shims/pip

三、入门

除了 frida 主程序外,frida-tools 里还提供了五个实用工具,它们位于 /usr/local/bin/ 目录下

ls -al /usr/local/bin/frida-*

如果使用的是 pyenvpip 安装的 frida,则它们会被安装到 /usr/local/var/pyenv/shims/ 目录下

ls -al /usr/local/var/pyenv/shims/frida-*
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-apk
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-create
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-discover
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-join
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-kill
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-ls-devices
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-ps
-rwxr-xr-x  1 lxf  admin  180  3 19 22:05 /usr/local/var/pyenv/shims/frida-trace

1、查看可用的设备列表

frida-ls-devices 用于获取可用的设备列表,在多设备交互的情况下会非常有用

➜  ~ frida-ls-devices
Id                                        Type    Name
----------------------------------------  ------  ------------
local                                     local   Local System
d007dc58edd70caad950ff01b41ebf73cfa49fbe  usb     iPad
socket                                    remote  Local Socket

2、获取设备的进程列表

frida-ps 用于获取进程列表信息

➜  ~ frida-ps --help
usage: frida-ps [options]

options:
  -h, --help            show this help message and exit
  -D ID, --device ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host HOST  connect to remote frida-server on HOST
  --certificate CERTIFICATE
                        speak TLS with HOST, expecting CERTIFICATE
  --origin ORIGIN       connect to remote server with “Origin” header set to
                        ORIGIN
  --token TOKEN         authenticate with HOST using TOKEN
  --keepalive-interval INTERVAL
                        set keepalive interval in seconds, or 0 to disable
                        (defaults to -1 to auto-select based on transport)
  --p2p                 establish a peer-to-peer connection with target
  --stun-server ADDRESS
                        set STUN server ADDRESS to use with --p2p
  --relay address,username,password,turn-{udp,tcp,tls}
                        add relay to use with --p2p
  -O FILE, --options-file FILE
                        text file containing additional command line options
  --version             show program's version number and exit
  -a, --applications    list only applications
  -i, --installed       include all installed applications
  -j, --json            output results as JSON

这里说明一下常用的命令参数

参数描述
-U连接到 USB 设备
-D如果当前有多台 USB 设备,可以使用该参数指定设备的 UDIDfrida-ls-devices 列出的那些 id
-R/-H连接到远程 frida-server,主要用于远程调试
-a仅显示正在运行的应用
-i显示所有已安装的应用(包括 AppStore安装的应用和系统应用)

具体使用如下:

连接到 USB 设备查看进程列表

~ frida-ps -U
  PID  Name
-----  ---------------------------------------------------
25226   Cydia
26745   Twitter
21611   邮件
25055      AppPredictionWidget
20944      AppleCredentialManagerDaemon
 1687      AssetCacheLocatorService
23387      CMFSyncAgent
...

连接到 USB 设备查看正在运行的应用

➜  ~ frida-ps -U -a
  PID  Name         Identifier
-----  -----------  --------------------
25226   Cydia    com.saurik.Cydia
26745   Twitter  com.atebits.Tweetie2
21611   邮件       com.apple.mobilemail
➜  ~

连接到 USB 设备查看所有安装的应用

➜  ~ frida-ps -U -a -i
  PID  Name                         Identifier
-----  ---------------------------  ------------------------------------------
25226   Cydia                    com.saurik.Cydia
26745   Twitter                  com.atebits.Tweetie2
21611   邮件                       com.apple.mobilemail
    -   App Store                com.apple.AppStore
    -   FaceTime 通话              com.apple.facetime
    -   LXFProtocolTool_Example  org.cocoapods.demo.LXFProtocolTool-Example
    -   Photo Booth              com.apple.Photo-Booth
    -   Safari 浏览器               com.apple.mobilesafari
    -   Substitute               com.ex.substitute.settings
    -   SwiftyFitsize_Swift      org.cocoapods.demo.SwiftyFitsize-Swift
    -   iTunes Store             com.apple.MobileStore
    -   信息                       com.apple.MobileSMS
    -   查找 iPhone                com.apple.mobileme.fmip1
    -   设置                       com.apple.Preferences
....

连接到指定的 USB 设备查看正在运行的应用

➜  ~ frida-ps -D d007dc58edd70caad950ff01b41ebf73cfa49fbe -a
  PID  Name         Identifier
-----  -----------  --------------------
25226   Cydia    com.saurik.Cydia
26745   Twitter  com.atebits.Tweetie2
21611   邮件       com.apple.mobilemail
➜  ~

3、杀死进程

frida-kill 用来结束设备上的指定进程

➜  ~ frida-kill --help
usage: frida-kill [options] process

options:
  -h, --help            show this help message and exit
  -D ID, --device ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host HOST  connect to remote frida-server on HOST
  --certificate CERTIFICATE
                        speak TLS with HOST, expecting CERTIFICATE
  --origin ORIGIN       connect to remote server with “Origin” header set to
                        ORIGIN
  --token TOKEN         authenticate with HOST using TOKEN
  --keepalive-interval INTERVAL
                        set keepalive interval in seconds, or 0 to disable
                        (defaults to -1 to auto-select based on transport)
  --p2p                 establish a peer-to-peer connection with target
  --stun-server ADDRESS
                        set STUN server ADDRESS to use with --p2p
  --relay address,username,password,turn-{udp,tcp,tls}
                        add relay to use with --p2p
  -O FILE, --options-file FILE
                        text file containing additional command line options
  --version             show program's version number and exit

举个例子,杀掉 PID26745Twitter

frida-kill -U 26745
frida-kill -U Twitter
frida-kill -D d007dc58edd70caad950ff01b41ebf73cfa49fbe 26745
frida-kill -D d007dc58edd70caad950ff01b41ebf73cfa49fbe Twitter

4、跟踪函数/方法的调用

frida-trace 用于跟踪函数或方法的调用。

➜  ~ frida-trace --help
usage: frida-trace [options] target

positional arguments:
  args                  extra arguments and/or target

options:
  -h, --help            show this help message and exit
  -D ID, --device ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host HOST  connect to remote frida-server on HOST
  --certificate CERTIFICATE
                        speak TLS with HOST, expecting CERTIFICATE
  --origin ORIGIN       connect to remote server with “Origin” header set to ORIGIN
  --token TOKEN         authenticate with HOST using TOKEN
  --keepalive-interval INTERVAL
                        set keepalive interval in seconds, or 0 to disable (defaults to -1 to auto-select based
                        on transport)
  --p2p                 establish a peer-to-peer connection with target
  --stun-server ADDRESS
                        set STUN server ADDRESS to use with --p2p
  --relay address,username,password,turn-{udp,tcp,tls}
                        add relay to use with --p2p
  -f TARGET, --file TARGET
                        spawn FILE
  -F, --attach-frontmost
                        attach to frontmost application
  -n NAME, --attach-name NAME
                        attach to NAME
  -p PID, --attach-pid PID
                        attach to PID
  -W PATTERN, --await PATTERN
                        await spawn matching PATTERN
  --stdio {inherit,pipe}
                        stdio behavior when spawning (defaults to “inherit”)
  --aux option          set aux option when spawning, such as “uid=(int)42” (supported types are: string, bool,
                        int)
  --realm {native,emulated}
                        realm to attach in
  --runtime {qjs,v8}    script runtime to use
  --debug               enable the Node.js compatible script debugger
  --squelch-crash       if enabled, will not dump crash report to console
  -O FILE, --options-file FILE
                        text file containing additional command line options
  --version             show program's version number and exit
  -I MODULE, --include-module MODULE
                        include MODULE
  -X MODULE, --exclude-module MODULE
                        exclude MODULE
  -i FUNCTION, --include FUNCTION
                        include [MODULE!]FUNCTION
  -x FUNCTION, --exclude FUNCTION
                        exclude [MODULE!]FUNCTION
  -a MODULE!OFFSET, --add MODULE!OFFSET
                        add MODULE!OFFSET
  -T INCLUDE_IMPORTS, --include-imports INCLUDE_IMPORTS
                        include program's imports
  -t MODULE, --include-module-imports MODULE
                        include MODULE imports
  -m OBJC_METHOD, --include-objc-method OBJC_METHOD
                        include OBJC_METHOD
  -M OBJC_METHOD, --exclude-objc-method OBJC_METHOD
                        exclude OBJC_METHOD
  -j JAVA_METHOD, --include-java-method JAVA_METHOD
                        include JAVA_METHOD
  -J JAVA_METHOD, --exclude-java-method JAVA_METHOD
                        exclude JAVA_METHOD
  -s DEBUG_SYMBOL, --include-debug-symbol DEBUG_SYMBOL
                        include DEBUG_SYMBOL
  -q, --quiet           do not format output messages
  -d, --decorate        add module name to generated onEnter log statement
  -S PATH, --init-session PATH
                        path to JavaScript file used to initialize the session
  -P PARAMETERS_JSON, --parameters PARAMETERS_JSON
                        parameters as JSON, exposed as a global named 'parameters'
  -o OUTPUT, --output OUTPUT
                        dump messages to file

4.1 跟踪函数调用

 ➜  ~ frida-trace -U -i compress -i "recv*" -x "recvmsg*" Twitter
Instrumenting...
compress: Auto-generated handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libz.1.dylib/compress.js"
recvfrom$NOCANCEL: Auto-generated handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_kernel.dylib/recvfrom_NOCANCEL.js"
recvfrom: Auto-generated handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_kernel.dylib/recvfrom.js"
recv: Auto-generated handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_c.dylib/recv.js"
recv$NOCANCEL: Auto-generated handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_c.dylib/recv_NOCANCEL.js"
Started tracing 5 functions. Press Ctrl+C to stop.

参数说明

参数描述
-i包含某个函数,支持模糊匹配
-x排除某个函数,支持模糊匹配

注:进行模糊匹配时,需要使用双引号进行包裹!

上述命令的意思:跟踪名为 compress 和以 recv 开头的函数,且排除以 recvmsg 开头的函数。

当跟踪的函数被触发时,会输出以下日志:

           /* TID 0x1bb43 */
 36078 ms  recv$NOCANCEL()
 36078 ms     | recvfrom$NOCANCEL()
 36081 ms  recv$NOCANCEL()
 36081 ms     | recvfrom$NOCANCEL()
 36082 ms  recv$NOCANCEL()
 36082 ms     | recvfrom$NOCANCEL()
 36083 ms  recv$NOCANCEL()
 36083 ms     | recvfrom$NOCANCEL()
 36083 ms  recv$NOCANCEL()
 36083 ms     | recvfrom$NOCANCEL()

命令在执行后会在当前目录下会生成一个名为 __handlers__ 的文件夹,里面存放的是自动生成的脚本文件

.
└── __handlers__
    ├── libsystem_c.dylib
    │   ├── recv.js
    │   └── recv_NOCANCEL.js
    ├── libsystem_kernel.dylib
    │   ├── recvfrom.js
    │   └── recvfrom_NOCANCEL.js
    └── libz.1.dylib
        └── compress.js

上述命令是在目标 App 打开后执行的,如果我们需要强制启动 App 来进行跟踪,可以使用 -f 应用的BundleID 参数,如:

➜  ~ frida-trace -U -i compress -i "recv*" -x "recvmsg*" -f "com.atebits.Tweetie2"
Instrumenting...
compress: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libz.1.dylib/compress.js"
recvfrom$NOCANCEL: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_kernel.dylib/recvfrom_NOCANCEL.js"
recvfrom: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_kernel.dylib/recvfrom.js"
recv: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_c.dylib/recv.js"
recv$NOCANCEL: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/libsystem_c.dylib/recv_NOCANCEL.js"
Started tracing 5 functions. Press Ctrl+C to stop.

注:frida-trace 执行时不会覆盖已有的脚本文件(即 __handlers__ 文件夹下的脚本),所以可以进行任意修改这些 JS 文件来添加想要的功能。

4.2 跟踪 OC 方法的调用

➜  Test frida-trace -U -m "-[T1HomeTimelineItemsViewController _load*]" -M "-[T1HomeTimelineItemsViewController _loadBottomWithSource:]" Twitter
Instrumenting...
-[T1HomeTimelineItemsViewController _loadTopWithSource:]: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/T1HomeTimelineItemsViewController/_loadTopWithSource_.js"
-[T1HomeTimelineItemsViewController _loadGap:withSource:]: Loaded handler at "/Users/lxf/Desktop/LXF/reverse/Test/__handlers__/T1HomeTimelineItemsViewController/_loadGap_withSource_.js"
Started tracing 2 functions. Press Ctrl+C to stop.
           /* TID 0x303 */
 15600 ms  -[T1HomeTimelineItemsViewController _loadTopWithSource:0xc8]

参数说明

参数描述
-m包含某个方法,支持模糊匹配
-M排除某个方法,支持模糊匹配

4.3 跟踪调用栈

只需要在 JS 文件中添加如下代码片段即可跟踪某个方法的调用栈

console.log('\tBacktrace:\n\t' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t'));

想了解详细的接口说明可以在 Frida 官网链接:frida.re/docs/javasc… 上找到。 

5、交互模式

frida 提供了两种进入交互模式的方式

5.1 通过应用名或 PID 附加

应用于 App 已打开的情况下附加的情景

frida -U 应用名
frida -U -p PID

当使用 PID 进行附加时,-p 可加可不加

举例:

frida -U Twitter
frida -U 26984
frida -U -p 26984

5.2 启动应用进入交互模式

应用于 App 未打开的情景

➜  Test frida -U -f com.atebits.Tweetie2
     ____
    / _  |   Frida 15.1.17 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to iPad (id=d007dc58edd70caad950ff01b41ebf73cfa49fbe)
Spawned `com.atebits.Tweetie2`. Use %resume to let the main thread start executing!
[iPad::com.atebits.Tweetie2 ]-> %resume

注:需要自己额外再输入 %resume,否则目标应用将一直处于暂停的状态。

如果启动应用后被强制退出或不想再额外输入 %resume,可以加上 --no-pause

frida -U -f com.atebits.Tweetie2 --no-pause

四、实战

针对上图中的【翻译推文】,我们来把这个标题和点击事件给修改掉

首先我们要做的就是视图组件定位,在这个页面下,使用 FLEX 工具便可轻松定位到

点击右侧的感叹号,可以看到该视图的属性和方法

然后通过如下代码,确认其是否为我们想要 hook 的方法

if (ObjC.available) {
    var didTap = ObjC.classes.T1TranslateButton['- _didTap:forEvent:']
    var setTitle = ObjC.classes.T1TranslateButton['- setTitleText:']
    Interceptor.attach(setTitleOldImp, {
      onEnter: function(args) {
        console.log("args 0 -- ", ObjC.Object(args[0]))
        console.log("args 2 -- ", ObjC.Object(args[2]))
      }
    })
    didTap.implementation = ObjC.implement(setTitle, function(handle, selector, arg1, arg2) {

      var self = ObjC.Object(handle)
      console.log("self -- ", self) 
    })
}

打开 Twitter 后,执行如下命令 frida -U -l Twitter.js Twitter

➜ frida -U -l Twitter.js Twitter
     ____
    / _  |   Frida 15.1.17 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to iPad (id=d007dc58edd70caad950ff01b41ebf73cfa49fbe)
[iPad::Twitter ]->

Twitter 进入到指定页面后输出:

args 0 --  <T1TranslateButton: 0x122e46f80; baseClass = UIButton; frame = (0 0; 66 22); opaque = NO; layer = <CALayer: 0x28372a760>>
args 2 --  翻译推文

点一下【翻译推文】按钮输出:

[iPad::Twitter ]-> self --  <T1TranslateButton: 0x122e46f80; baseClass = UIButton; frame = (0 0; 66 22); opaque = NO; layer = <CALayer: 0x28372a760>>

看来是没错了,那接下来,我们把标题和点击事件进行修改,完整代码如下:

if (ObjC.available) {
    const { NSString } = ObjC.classes;
    var UIAlertController = ObjC.classes.UIAlertController;
    var UIAlertAction = ObjC.classes.UIAlertAction;
    var UIApplication = ObjC.classes.UIApplication;

    // 弹窗
    function showAlert() {
      var alertHandler = new ObjC.Block({ retType: 'void', argTypes: ['object'], implementation: function () {} });
    
      ObjC.schedule(ObjC.mainQueue, function () {
        var alert = UIAlertController.alertControllerWithTitle_message_preferredStyle_('LinXunFeng', '欢迎关注公众号:FSA全栈行动\n博客:https://fullstackaction.com', 1);
        var defaultAction = UIAlertAction.actionWithTitle_style_handler_('OK', 0, alertHandler);
        alert.addAction_(defaultAction);
        UIApplication.sharedApplication().keyWindow().rootViewController().presentViewController_animated_completion_(alert, true, NULL);
      })
    }

    // 播放系统声音
    function playSystemSound() {
      var playSound = new NativeFunction(Module.findExportByName('AudioToolbox', 'AudioServicesPlaySystemSound'), 'void', ['int'])
      playSound(1111)
    }

    var didTap = ObjC.classes.T1TranslateButton['- _didTap:forEvent:']
    var setTitle = ObjC.classes.T1TranslateButton['- setTitleText:']
    
    // 保留旧实现
    var didTapOldImp = didTap.implementation

    // hook
    Interceptor.attach(setTitleOldImp, {
      onEnter: function(args) {
        args[2] = ptr(NSString.stringWithString_("Hello LinXunFeng,点击我来弹个窗和听个曲吧"))
      }
    })

    // 覆盖实现
    didTap.implementation = ObjC.implement(setTitle, function(handle, selector, arg1, arg2) {
      // 调用旧实现
      // didTapOldImp(handle, selector, arg1, arg2)
      
      playSystemSound()
      showAlert()
    })
}

五、进阶

1、Python 交互

Frida 提供了 PythonJS 脚本的交互

1.1、获取设备

import frida

if __name__ == '__main__':

    deviceManager = frida.get_device_manager()

    # 枚举所有连接的设备
    print(deviceManager.enumerate_devices())

    # 根据 UDID 获取设备
    print(deviceManager.get_device("d007dc58edd70caad950ff01b41ebf73cfa49fbe"))

    # 获取当前 USB 连接的设备
    print(frida.get_usb_device())

运行结果:

[Device(id="local", name="Local System", type='local'), Device(id="socket", name="Local Socket", type='remote'), Device(id="d007dc58edd70caad950ff01b41ebf73cfa49fbe", name="iPad", type='usb')]
Device(id="d007dc58edd70caad950ff01b41ebf73cfa49fbe", name="iPad", type='usb')
Device(id="d007dc58edd70caad950ff01b41ebf73cfa49fbe", name="iPad", type='usb')

1.2、附加进程

使用 attach() 附加进程,得到 Session 实例

if __name__ == '__main__':
    device = frida.get_usb_device()
    session = device.attach("Twitter")  # 进程名
    # session = device.attach(27489)  # PID
    print(session)

输出内容:

Session(pid=27489)

1.3、启动进程

使用 spawn 可以启动进程,不过会进入挂起状态,需要配合 resume() 方法才能唤醒

if __name__ == '__main__':
    device = frida.get_usb_device()
    pid = device.spawn("com.atebits.Tweetie2")
    # session = device.attach(pid)
    device.resume(pid)

spawn 可携带参数运行

如下方代码所示,运行 Safari 并打开 FSA全栈行动 博客: https://fullstackaction.com

pid = device.spawn("com.apple.mobilesafari", url="https://fullstackaction.com/")
device.resume(pid)

1.4、脱离进程

得到 Session 并完成所有操作后,需要使用 detach() 脱离进程

if __name__ == '__main__':
    device = frida.get_usb_device()
    pid = device.spawn("com.atebits.Tweetie2")
    session = device.attach(pid)
    device.resume(pid)

    session.detach()  # 脱离进程

1.5、注入 JS 脚本

得到 Session 实例后,就可以调用其 create_script 方法创建一个脚本对象,再调用该脚本对象的 load 方法进行脚本注入

if __name__ == '__main__':
    device = frida.get_usb_device()
    pid = device.spawn("com.atebits.Tweetie2")
    session = device.attach(pid)
    device.resume(pid)

    script = session.create_script("""
    if (ObjC.available) {
        var NSHomeDirectory = new NativeFunction(ptr(Module.findExportByName("Foundation", "NSHomeDirectory")), 'pointer', []);
        var path = new ObjC.Object(NSHomeDirectory());
        console.log(path);
    }
    """)
    script.load()
    session.detach()

JS 脚本可以保存到本地文件中再进行读取:

with codecs.open('./xxx.js', 'r', 'utf-8') as f:
    source = f.read()
script = session.create_script(source)

1.6、PythonJS 交互

JS 端传递参数,JS 端处理完成后将结果返回给 Python 端,这种场景还是很常见的,那应该怎么做呢?

示例代码如下:

import frida
import threading

g_event = threading.Event()  # 同步


def payload_message(payload):
    # print("payload_message -- ", payload)
    if "msg" in payload:
        print(payload["msg"])

    if 'status' in payload:
        if payload['status'] == 'success':
            g_event.set()


def on_message(message, data):
    # print("on_message message -- ", message)
    if message['type'] == 'send':
        payload_message(message['payload'])
    elif message['type'] == 'error':
        print(message['stack'])


SCRIPT_JS = ("""
    function handleMessage(message) {
        var cmd = message['cmd'] 
        if (cmd == 'GetDirectory') {
            var name = message['name']
            var path;
            switch (name) {
            case 'home':
                var NSHomeDirectory = new NativeFunction(ptr(Module.findExportByName("Foundation", "NSHomeDirectory")), 'pointer', []);
                path = new ObjC.Object(NSHomeDirectory());
                break;
            case 'tmp':
                var NSTemporaryDirectory = new NativeFunction(ptr(Module.findExportByName("Foundation", "NSTemporaryDirectory")), 'pointer', []);
                path = new ObjC.Object(NSTemporaryDirectory());
                break;
            default:
                path = "写的啥呀"
            }
            if (path) send({msg: path.toString()});
        }   
        send({status: 'success'});
    }
    recv(handleMessage);
""")

# 根据名字获取对应的沙盒路径
def getDirectory(target_process, name):
    device = frida.get_usb_device()
    session = device.attach(target_process)
    script = session.create_script(SCRIPT_JS)
    script.on('message', on_message)
    script.load()
    script.post({'cmd': 'GetDirectory', 'name': name})
    g_event.wait()
    session.detach()


if __name__ == '__main__':
    # getDirectory('Twitter', 'home')
    getDirectory('Twitter', 'tmp')
  1. g_event 是为了保证同步
  2. JS 端可以设置 recv() 的回调接收 Python 端的消息
  3. Python 端通过 script.on() 设置回调,再使用 script.post() 将参数传递给 JS 端,然后调用 g_event.wait() 进入等待状态
  4. JS 端内部处理完成后,使用 send(){status: 'success'} 传递给 Python
  5. on_message 回调中取到 JS 端返回的数据,当识别到 statussuccess 后,调用 g_event.set() 使主线程继续执行

2、拦截某个类的所有方法

如果想对某个类的所有方法进行批量拦截,可以使用 ApiResolver 接口,它可以根据正则表达式获取符合条件的所有方法

var resolver = new ApiResolver('objc')
resolver.enumerateMatches('*[T1TranslateButton *]', {
    onMatch: function (match) {
        console.log(match['name'] + ":" + match['address'])
    },
    onComplete: function () {}
})

输出结果:

+[T1TranslateButton tfn_defaultShouldFlipForRightToLeftTransform]:0x101be4014
+[T1TranslateButton button]:0x101be2774
-[T1TranslateButton tapActionBlock]:0x101be4280
-[T1TranslateButton setTapActionBlock:]:0x101be4290
-[T1TranslateButton translationSource]:0x101be4250
-[T1TranslateButton setLogoTapActionBlock:]:0x101be42ac
-[T1TranslateButton setShowingTranslation:]:0x101be2c10
-[T1TranslateButton setOriginalLanguage:]:0x101be2b58
-[T1TranslateButton setTranslationSource:]:0x101be2cec
-[T1TranslateButton _didTap:forEvent:]:0x101be296c
-[T1TranslateButton _t1_didHover:]:0x101be4140
-[T1TranslateButton setSelectionPadding:]:0x101be42d8
-[T1TranslateButton touchRect]:0x101be42f8
-[T1TranslateButton _t1_isTouchingLogo:]:0x101be2a8c
-[T1TranslateButton touchLogoRect]:0x101be4328
-[T1TranslateButton _t1_buttonTitle]:0x101be2f28
-[T1TranslateButton autoTranslationExpanded]:0x101be4270
-[T1TranslateButton _t1_imageHeightOffsetForLogo:]:0x101be3164
-[T1TranslateButton _t1_titleRectWithTitleString:origin:]:0x101be32b8
-[T1TranslateButton _t1_imageRectWithOrigin:]:0x101be3394
-[T1TranslateButton _t1_drawRectFor:]:0x101be3f94
-[T1TranslateButton setTouchRect:]:0x101be4310
-[T1TranslateButton _t1_drawHighlightWithContext:andRect:]:0x101be401c
-[T1TranslateButton setTouchLogoRect:]:0x101be4340
-[T1TranslateButton setAutoTranslationExpanded:]:0x101be2cc4
-[T1TranslateButton originalLanguage]:0x101be4240
-[T1TranslateButton showingTranslation]:0x101be4260
-[T1TranslateButton logoTapActionBlock]:0x101be429c
-[T1TranslateButton selectionPadding]:0x101be42c8
-[T1TranslateButton _dynamicColorsDidReload:]:0x101be3ed0
-[T1TranslateButton titleText]:0x101be42b8
-[T1TranslateButton _titleColor]:0x101be2ea8
-[T1TranslateButton logoImage]:0x101be42e8
-[T1TranslateButton _logoImage]:0x101be2d14
-[T1TranslateButton dealloc]:0x101be28e4
-[T1TranslateButton .cxx_destruct]:0x101be4358
-[T1TranslateButton initWithFrame:]:0x101be27dc
-[T1TranslateButton sizeThatFits:]:0x101be3408
-[T1TranslateButton setHighlighted:]:0x101be2b08
-[T1TranslateButton drawRect:]:0x101be3798
-[T1TranslateButton setTitleText:]:0x101be2c38

3、替换原方法

Interceptor.attach() 可以在拦截目标后,打印参数,修改返回值,但无法阻止原方法的执行

我们可以给原方法的 implementation 进行赋值,从而覆盖其实现

var didTap = ObjC.classes.T1TranslateButton['- _didTap:forEvent:']

var didTapOldImp = didTap.implementation

// 覆盖实现
didTap.implementation = ObjC.implement(setTitle, function(handle, selector, arg1, arg2) {

  var self = ObjC.Object(handle)
  console.log("self -- ", self) 

  // 调用旧实现
  // didTapOldImp(handle, selector, arg1, arg2)
})

这里需要注意的是,像 _didTap:forEvent: 这里需要传递两个参数,则 ObjC.implement 的回调中也需要写明两个参数(arg1arg2),即需要多少参数就写多少,没有则不用写

4、RPC 调用

RPC:即 Remote Procedure Call,远程过程调用,开发人员可以将封装好的任意函数指定为 RPC 函数,以提供给 Python 使用。

利用 rpc.exports = {} 导出 RPC 函数,多个函数以逗号分隔,注意:方法名需要全小写!

function getHomeDirectory() {
    var NSHomeDirectory = new NativeFunction(ptr(Module.findExportByName("Foundation", "NSHomeDirectory")), 'pointer', [])
    var path = new ObjC.Object(NSHomeDirectory());
    return path.toString()
}

function openUrl(url) {
    var UIApplication = ObjC.classes.UIApplication.sharedApplication()
    var toOpen = ObjC.classes.NSURL.URLWithString_(url)
    return UIApplication.openURL_(toOpen)
}

function playSystemSound() {
    var playSound = new NativeFunction(Module.findExportByName('AudioToolbox', 'AudioServicesPlaySystemSound'), 'void', ['int'])
    playSound(1111)
}

// 导出 RPC 函数
rpc.exports = {
    openurl: function (url) {
        openUrl(url)
    },
    sound: function () {
        playSystemSound()
    },
    alert: function () {
        showAlert()
    },
    homedirectory: function () { // homedirectory 必须小写
        return getHomeDirectory()
    }
}

Python 端使用 rpc.js

import codecs
import frida

if __name__ == '__main__':
    device = frida.get_usb_device()
    session = device.attach('Twitter')

    # 读取 JS 脚本 
    with codecs.open('./rpc.js', 'r', 'utf-8') as f:
        source = f.read()

    script = session.create_script(source)
    script.load()

    rpc = script.exports
    rpc.openurl("https://fullstackaction.com")
    rpc.sound()
    print(rpc.homeDirectory())
    # print(rpc)

    session.detach()

六、最后

以上代码已经上传至:github.com/LinXunFeng/…

Frida 提供的 API 接口十分丰富,这里只提到了常用的内容,更多内容还是需要我们一起去阅读官方文档:frida.re/docs/javasc…

除此之外,codeshare.frida.re 上提供很多共享脚本,大家可以用来学习和引入使用