苹果APP逆向神器Frida详细使用教程

一、简介

Frida是一个跨平台的轻量级Hook框架,支持MacOS、Linux和Windows操作系统,提供了精简的Python接口和功能丰富的JS接口,除了可以使用自身的控制台交互以外,还可以使用Python将JS脚本注入到运行程序中,通过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

更简单的安装可以参考这篇文章

Mac苹果端Frida-ios-dump一键砸壳

三、入门

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

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

如果使用的是pyenv的pip安装的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 设备,可以使用该参数指定设备的UDID(frida-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

举个例子,杀掉PID 为26745的Twitter

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/javascript-api/#thread上找到。

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

四、实战


针对上图中的【翻译推文】,我们来把这个标题和点击事件给修改掉
首先我们要做的就是视图组件定位,在这个页面下,使用FLEXLIST工具便可轻松找定位到

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


然后通过如下代码,确认其是我们想要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 --  >
args 2 --  翻译推文

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

[iPad::Twitter ]-> self --  >

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

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提供了Python和JS脚本的交互
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并打开天赐网络博客: https://www.vlwx.com

pid = device.spawn("com.apple.mobilesafari", url="https://www.vlwx.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、Python与JS交互
向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端返回的数据,当识别到status为success后,调用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的回调中也需要写明两个参数(arg1、arg2),即需要多少参数就写多少,没有则不用写

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
Frida提供的API接口十分丰富,这里只提到了常用的内容,更多内容还是需要我们一起去阅读官方文档:frida.re/docs/javascript-api
除此之外,codeshare.frida.re上提供很多共享脚本,大家可以用来学习和引入使用

给TA打赏
共{{data.count}}人
人已打赏
技术文档

Mac苹果端Frida-ios-dump一键砸壳

2022-5-7 22:23:01

技术文档

宝塔面板Nginx禁止IP访问80、443端口

2022-5-13 1:36:48

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
有新私信 私信列表
搜索