iOS逆向 - 运行时分析(三)Frida
# 一、简介
Frida
是一个跨平台的轻量级 Hook
框架,支持 MacOS
、Linux
和 Windows
操作系统,提供了精简的 Python
接口和功能丰富的 JS
接口,除了可以使用自身的控制台交互以外,还可以使用 Python
将 JS
脚本注入到运行程序中,通过 Frida
可以获取程序的详细信息、拦截和调用指定函数、注入代码、修改参数等。
Frida
源代码托管:https://github.com/frida (opens new window)
# 二、安装
# 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-*
如果使用的是 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
官网链接:https://frida.re/docs/javascript-api/#thread (opens new window) 上找到。
# 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
提供了 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
并打开 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、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')
g_event
是为了保证同步JS
端可以设置recv()
的回调接收Python
端的消息Python
端通过script.on()
设置回调,再使用script.post()
将参数传递给JS
端,然后调用g_event.wait()
进入等待状态JS
端内部处理完成后,使用send()
将{status: 'success'}
传递给Python
端- 在
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()
# 六、最后
以上代码已经上传至:https://github.com/LinXunFeng/frida_study (opens new window)
Frida
提供的 API
接口十分丰富,这里只提到了常用的内容,更多内容还是需要我们一起去阅读官方文档:https://frida.re/docs/javascript-api (opens new window)
除此之外,https://codeshare.frida.re (opens new window) 上提供很多共享脚本,大家可以用来学习和引入使用
- 01
- Flutter - 子部件任意位置观察滚动数据11-24
- 02
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 03
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21