frida用法
0x0 About
记录适用于ios的objective-c的类中函数的frida的用法
0x1 必读
0x2 安装
pip3 install frida
或
从这里下载 https://build.frida.re/frida/
0x3 frida的常见用法
- 0xa hook函数(ios中theos具备的功能)
- 0xb 记录函数执行日志(ios中theos具备的功能)
- 0xc 调用函数(ios中cycript具备的功能)
- 0xd 读写内存(类似于调试器的功能)
- 0xe 查看对象属性
使用frida有两种方法
- 命令行[实现简单功能推荐]:
frida -U -p pid -l xxx.js
- api调用[实现复杂功能推荐]: 例如使用python调用frida的python api来写python脚本实现相应功能,使用python调用api时,推荐阅读api实现细节frida/core.py,frida/tracer.py.
上面的4种用法可能会涉及到如下js
for (var className in ObjC.classes)
{
if (ObjC.classes.hasOwnProperty(className))
{
console.log(className);
}
}
console.log("[*] Started: Find All Methods of a Specific Class");
if (ObjC.available)
{
try
{
var className = "xxx";
var methods = eval('ObjC.classes.' + className + '.$methods');
for (var i = 0; i < methods.length; i++)
{
try
{
console.log("[-] "+methods[i]);
}
catch(err)
{
console.log("[!] Exception1: " + err.message);
}
}
}
catch(err)
{
console.log("[!] Exception2: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
console.log("[*] Completed: Find All Methods of a Specific Class");
if (ObjC.available)
{
try
{
var className = "xxx";
var funcName = "- xxx";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName);
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
//注意:有时args参数值在这里会是一个对象,如果函数返回值是字符串类型,为了更好理解则要这样写
//这里假设args[2]是要记录的参数
//ObjC.classes.NSString.stringWithString_(args[2])或者args[2].toString()或者ObjC.classes.NSString.stringWithString_(args[2]).toString()
//具体情况需要测试下是上面这3种的哪种写法
console.log("param:"+args[2]+" type:"+typeof args[2]);
},
onLeave: function(retval) {
//注意:retval一般会返回一个对象,如果函数返回值是字符串类型,为了更好理解则要这样写
//ObjC.classes.NSString.stringWithString_(retval)或者retval.toString()或者ObjC.classes.NSString.stringWithString_(retval).toString()
//具体情况需要测试下是上面这3种的哪种写法
console.log("Return value-> (type:"+typeof retval+",value:"+retval+")");
}
});
}
catch(err)
{
console.log("[!] Exception2: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
if (ObjC.available)
{
try
{
var className = "PARSPedometerInfo";
var funcName = "- integratedSteps";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
console.log("Original args0-> (type:"+typeof args[0]+",value:"+args[0]+")");
newargs0=ptr('xxx')
args[0]=newargs0
console.log("New args0-> (type:"+typeof args[0]+",value:"+args[0]+")");
},
onLeave: function(retval) {
//注意:retval永远是一个对象,如果函数返回值是字符串类型,为了更好理解则要这样写
//string_value=ObjC.classes.NSString.stringWithString_(retval)
//console.log("Original return value-> (type:"+typeof string_value+",value:"+string_value+")");
//newretval=ObjC.classes.NSString.stringWithString_("xxxx")
//retval.replace(newretval)
//console.log("New return value-> (type:"+typeof newretval+",value:"+newretval+")");
console.log("Origin return value-> (type:"+typeof retval",value:"+retval+")");
newretval=ptr("xxxx")
retval.replace(newretval)
console.log("New return value-> (type:"+typeof newretval",value:"+newretval+")");
}
});
}
catch(err)
{
console.log("[!] Exception2: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
0xa hook函数
0x1 正常hook函数
frida的hook函数功能在ios逆向中相当于theos的hook函数时修改函数的功能(theos相当于打补丁),但是比theos更方便,不用打包成deb安装,缺点是hook函数的语法没有theos简单.hook函数可通过命令行实现(可通过js热切换功能),也可通过其他语言调用frida的api来实现,通过其他语言(如python)调用api来实现时可做到更加”自动化”.在hook函数前要先找到要hook的函数名,可以通过frida枚举类中所有方法来查找或是通过ida反汇编后查看functions窗口
得到,其中通过frida得到的类中的函数名比通过ida得到的类中的函数名更全,因为frida会得到父类的函数,而ida的functions窗口看不到父类的函数名,如果只看ida中类的函数则会错失很多从父类继承的函数名.为了hook一个类中的函数,首先要找到这个类名和函数名,一般找类名是通过ida来分析得出,找到类名后再找未知的函数名时建议通过frida查找(这里是指静态分析时根据函数名猜测实现的对应功能,如果是动态调试则不用根据函数名来猜测对应功能).
- 找类
frida -U -p pid -l classes.js
- 找类函数
frida -U -p pid -l methodofclass.js
- hook函数
frida -U -p pid -l overwrite.js
,也可通过python api如下实现,其中js通过send()
传递内容到python中
之前由于在测试时发现ios objective-c的函数如果有参数,打印参数发现不是真的参数,在hook时修改函数参数也会不成功,详情,于是以为frida有bug,后来发现是参数序号写错了,ida中该函数如下
void __cdecl -[PARSPedometerInfo setIntegratedSteps:](PARSPedometerInfo *self, SEL a2, signed __int64 a3)
{
self->_integratedSteps = a3;
}
在上面函数中,a3参数正确的序号是2,(虽然在theos中hook这个函数时只需填写一个参数,[ObjC: args[0] = self, args[1] = selector, args[2-n] = arguments
]),最后代码如下:
import frida
import sys
session = frida.get_usb_device().attach(797)
script_string = """
if (ObjC.available)
{
try
{
var className = "PARSPedometerInfo";
var funcName = "- integratedSteps";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
console.log("Original args0-> type:"+typeof args[0]+" value:"+args[0])
newargs0=ptr('xxx')
args[0]=newargs0
console.log("New args0-> type:"+typeof args[0]+" value:"+args[0]")
send(args[0]);
},
onLeave: function(retval) {
console.log("Original retval-> type:"+typeof args[0]+" value:"+args[0])
newretval=ptr("xxxx")
retval.replace(newretval)
console.log("New retval-> type:"+typeof args[0]+" value:"+newretval)
send(newretval)
}
});
}
catch(err)
{
console.log("[!] Exception2: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
"""
script = session.create_script(script_string)
def on_message(message, data):
if message['type'] == 'error':
print("[!] " + message['stack'])
elif message['type'] == 'send':
print("[i] " + message['payload'])
else:
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
0x2 在函数内部hook
以上是正常的在函数刚进入或刚结束的时候进行hook,但有时候需要在函数的某一行代码上hook,这种也称为内联hook,例如要在某个函数内部的某个地址(以下为hookAddr变量)处打印eax和ebx的值,则不需要涉及这个函数的地址,直接hook这个函数内部的目标地址即可,相当于在该地址上下断点,如下:
// 基础函数地址
var baseAddr = ptr('0x12345678');
// 要 hook 的具体地址(基址 + 28 字节偏移)
var hookAddr = baseAddr.add(28);
Interceptor.attach(hookAddr, {
onEnter: function(args) {
// 获取当前的 context
var context = this.context;
// 打印 eax 和 ebx 的值
console.log('EAX: ' + context.eax.toString(16));
console.log('EBX: ' + context.ebx.toString(16));
}
});
0xb 记录函数执行日志
frida的记录函数执行日志功能在ios逆向中相当于theos的hook函数时记录函数参数和返回值的功能(theos相当于打补丁),优缺点同上面的0xa hook函数
.
frida -U -p pid -l record.js
,或用如下python脚本
import frida
import sys
session = frida.get_usb_device().attach(797)
script_string = """
if (ObjC.available)
{
try
{
var className = "xxx";
var funcName = "- xxx";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName);
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
console.log("param:"+args[0]+" type:"+typeof args[0]);
},
onLeave: function(retval) {
console.log("retval:"+retval+" type:"+typeof retval);
}
});
}
catch(err)
{
console.log("[!] Exception2: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
"""
script = session.create_script(script_string)
script.load()
sys.stdin.read()
关于记录函数调用有2个要注意的:
1.批量记录多个类里面所有函数的调用:
`frida-trace -U pid_value -m "-[regStr *:]"`
例如下面这条命令将记录所有的类中包含value(区分大小写)字符的所有函数的调用:
`frida-trace -U pid_value -m "-[* *value*:]"`
2.打印函数参数或返回值的内容
打印参数的时候可以直接这样打印:
var args2=ObjC.Object(args[2])
console.log(args2)
如果无法打印成功,可以先判断要打印的数据是什么数据类型:
console.log('Type of args[2] -> ' + new ObjC.Object(args[2]).$className)
得到类型后再根据不同的类型的打印方法来打印
常见类型的打印:
a)NSArray
var array = new ObjC.Object(args[2]);
/*
* Be sure to use valueOf() as NSUInteger is a Number in
* 32-bit processes, and UInt64 in 64-bit processes. This
* coerces it into a Number in the latter case.
*/
var count = array.count().valueOf();
for (var i = 0; i !== count; i++) {
var element = array.objectAtIndex_(i);
console.log(element)
}
b)NSArrayM
console.log(ObjC.classes.NSMutableArray.arrayWithObject_(args[2]));
更多的打印不同类型数据的用法可参考官网,很全:https://frida.re/docs/examples/ios/
如果是特殊的数据类型(非常规的列表,字典,字符串),比如第三方构造的类对象结构,可能要参考这里用的cast:
https://bbs.pediy.com/thread-253994.htm
https://github.com/frida/frida-core/issues/7
0xc 调用函数
0x1 ios/macOS
+
号开头的函数(类函数,可直接通过类调用)要这样:
id __cdecl +[NSString stringWithStrings:](NSString_meta *self, SEL a2, id a3)
{
__int64 v3; // x20
void *v4; // x0
void *v5; // x19
...
}
[NSString stringWithString:@"Hello World"]
becomes
var NSString = ObjC.classes.NSString;
NSString.stringWithString_("Hello World");
-
号开头的函数(实例函数,需要通过类的实例对象调用)要这样:
如下代码中调用了PARSHealthPedometer10thHomeViewController类中的requestUploadWithSure函数,这里的requestUploadWithSure函数是-
号开头的函数,其中函数的参数为1,如果requestUploadWithSure函数没有参数,则写法为my_obj["- requestUploadWithSure"]()
,如果要调用一个-
号开头的类的函数,有2种情况:
- 在内存中还没有类的一个实例(对象)
这种情况需要手动生成一个实例,用法为ObjC.classes.类名.alloc()
,在下面的代码中对应var my_obj=ObjC.classes.PARSHealthPedometer10thHomeViewController.alloc()
,其中alloc()
的作用是生成一个类的对象
- 在内存中已经有类的实例(对象)
这种情况需要先找出一个类的实例,使用var tmp=ObjC.chooseSync(ObjC.classes.类名)
,在下面的代码中对应var tmp=ObjC.chooseSync(ObjC.classes.PARSHealthPedometer10thHomeViewController)[0]
,其中[0]
表示取找到的实例中的第一个实例,可根据实际情况换成其他的实例
import frida
import sys
session = frida.get_usb_device().attach(897)
script_string = """
if (ObjC.available)
{
try
{
//var my_obj=ObjC.chooseSync(ObjC.classes.PARSHealthPedometer10thHomeViewController)[0]
var my_obj=ObjC.classes.PARSHealthPedometer10thHomeViewController.alloc()
my_obj["- requestUploadWithSure:"](1)
}
catch(err)
{
console.log("[!] Exception2: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
"""
script = session.create_script(script_string)
def on_message(message, data):
if message['type'] == 'error':
print("[!] " + message['stack'])
elif message['type'] == 'send':
print("[i] " + message['payload'])
else:
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
"""
script = session.create_script(script_string)
script.load()
sys.stdin.read()
0x2 windows
windows中不能直接调用内存中的地址,要先创建一个NativeFunction对象再通过js来调用,例如通过ida pro反编译得到一个函数如下:
int __thiscall sub_????????(int this, int a2, int a3)
{
int v3; // edx
int result; // eax
v3 = *(_DWORD *)(this + 30);
result = sub_2302450(a2,a3,v3);
return result;
}
用frida js来调用这个函数应该如下操作(下面假设在hook一个函数的时候调用另外一个函数,且这两个函数的this指针相同):
session = frida.attach(target_process_pid)
script = session.create_script("""
var baseAddr = Module.findBaseAddress('xx.exe');
var hook_func_addr=baseAddr.add(0x????????)
var target_func_address=baseAddr.add(0x12345678)
var target_func=new NativeFunction(target_func_address,'int',['pointer','int','int'],'thiscall')
Interceptor.attach(hook_func_addr, {
onEnter: function (args) {
//this指针保存在ecx中
var result=ptr(target_func(this.context.ecx,0,0)).readCString()
}
});
""")
script.on('message', on_message)
script.load()
0xd 读写内存
- 获取模块加载基址
可在ida调试ios app时替代vmmap获取加载基址.获取ios app加载基址用法如下:
import frida
#session = frida.get_usb_device().attach(pid_int_value)
session = frida.get_usb_device().attach("PALxxx")
print(session.enumerate_modules())
或者
import frida
#session = frida.get_usb_device().attach(pid_int_value)
session = frida.get_usb_device().attach("PALxxx")
base_addr=session.find_base_address("PALxxx")
print('0x%x' % base_addr)
- 枚举内存范围
s.enumerate_ranges('rw-')
:可找出可读可写的内存
s.enumerate_ranges('rwx')
:可找出可读可写可执行的内存
- read/write memory
s.read_bytes(4468416512, 5)
,等同于s.read_bytes(0x10a56a000, 5)
:在0x10a56a000上读5个字节
s.write_bytes(4468416512, "frida")
:在0x10a56a000上写入”frida”
一个读内存应用实例利用Frida从TeamViewer内存中提取密码
0xe 查看对象属性
$kind: string specifying either instance, class or meta-class
$super: an ObjC.Object instance used for chaining up to super-class method implementations
$superClass: super-class as an ObjC.Object instance
$class: class of this object as an ObjC.Object instance
$className: string containing the class name of this object
$moduleName: string containing the module path of this object
$protocols: object mapping protocol name to ObjC.Protocol instance for each of the protocols that this object conforms to
$methods: array containing native method names exposed by this object’s class and parent classes
$ownMethods: array containing native method names exposed by this object’s class, not including parent classes
$ivars: object mapping each instance variable name to its current value, allowing you to read and write each through access and assignment
一个对象(instance)可以通过.$methods查看它有哪些可调用的函数,可以通过.$ivars查看它有哪些属性和属性的值
0xf 对象操作
1)指针转对象
var obj=Objc.Object(ptr("0x600156041b0"))
2)对象获取
var obj=ObjC.classes.类名.alloc()
或者
var obj=ObjC.chooseSync(ObjC.classes.类名)[0]
3)对象函数调用
obj["- requestSomething:args1:args2:args3:"](args1v,args2v,args3v)
或者
obj.requestSomething_args1_args2_args3_(a1,a2,a3)
或者
obj.funcNameWithoutArg()
0x4 实战
0xa 理解brida
为了理解brida,需要理解pyro,参考这两个个连接(1,2)可理解pyro.理解pyro后再理解brida,理解brida后再看看brida中用到的pyro与这里的hrida,发现只是区别如下
brida:通过pyro开启rpc
hrida:通过http服务开rpc
这里brida和hrida开rpc的意义在于可以给网络上的其他用户使用本地的frida提供的的调用app中函数的的接口,但是frida已经有了rpc,为什么brida和hrida还要重新开rpc呢?因为frida的rpc只能给本地的其他程序使用(frida的rpc功能也可通过动态修改js内容和send修改后的js的执行结果给python来实现,因为frida加载js会热更新,这样修改js时可修改函数调用,并将函数执行结果发送给python),不能给网络上的其他程序使用,brida开rpc的意义不是很大,因为brida开rpc后是给本地的安全人员写的burp插件使用的,除非安全人员有几个好友,这样这些好友之间就可只要求其中一个人开启brida的rpc服务,其他人使用这个人的brida的rpc即可.frida注入app后可通过开rpc给网络上的其他用户提供调用app函数的接口,例如可通过本地frida注入某上传步数的App后将上传步数的接口通过rpc提供给网络上的人使用
brida工作原理如下图
也即,brida调用frida的rpc后通过pyro开启一个rpc给安全人员写的burp插件用,目前笔者认为brida只能用于辅助对称加密(如aes)的加密后的数据,也即brida工作时,人工调用brida的解密功能解密数据后再修改数据,然后将修改后的数据再用brida的加密功能加密好,加密好之后再通过burpsuite发送给正常的远程服务器.如果要通过burpsuite来修改非对称加密(如rsa)的加密后的数据需要使用下面的方法
上面这个链接里的思路是这样的:
App(frida)在数据加密前将明文发给burp,burp在人为修改数据后发给回显服务程序,回显服务程序打印burp发来的修改后的数据后,将这个数据发给App(frida).App(frida)收到回显服务程序发来的数据后看看这个数据是哪来的,如果是app没加密之前的数据就发给burp,如果是回显服务程序发来的数据就进行加密,然后再发给正常的服务器进行正常的请求.
上面链接里的方法看上去只能用于整个post的data部分被加密
的情况,不能用于部分get或post的参数值被加密
的情况,后来笔者写了一个工具xenc,可用于2种情况下的加密测试
- 1.get或post内容形如
mobilePhone=Nns7415cyOT0FkzwbjiXmahxvFt6tfw1Dda8id=1&a=2
- 2.post的整个data部分内容形如
<ÜñCHÁ*'-»84}_9Óûû84}_9Óûû884}_9Óûû84}_9Óû_9Óûû
0xb 实战”绕过rsa”
这里用xenc来测试部分get或post的参数值被加密
的情况
注意:手机不要黑屏,app要保证在前台不能在后台运行,黑屏或app进了后台会导致python调用ios app中的加密函数会超时,超时后可将手机打开可继续使用app的加密函数,最好保持手机不自动黑屏并保证app不进后台
js文件如下,其中encrypt
函数是一个rsa加密函数:
'use strict';
rpc.exports = {
encrypt: function (plain) {
var result_obj=ObjC.chooseSync(ObjC.classes.PARSCryptDataUtils)[0].encryptWithServerTimestamp_type_date_(a1,a2,a3)
return result.toString()
},
add: function (a, b) {
return a + b;
}
};
burpsuite的repeater中的request内容如下(其中mobilePhone在正常通信时被加密):
POST /xxx.do HTTP/1.1
Host: 127.0.0.1:8888
Real-Host: xxx.xxx.xxx
Connection: close
Accept: */*
User-Agent: PALxxx/4.11.0 (iPhone; iOS 9.0; Scale/2.00)
Accept-Language: zh-Hans-CN;q=1
X-Tingyun-Id: s8-utloiNb8;c=2;r=736688779
Content-Type: application/x-www-form-urlencoded
Content-Length: 993
mobilePhone=17634526787&id=1&b=2
运行xenc.py后设置要加密的参数为mobilePhone,且它的加密函数为encrypt
函数(在上面的js中有两个加密函数encrypt
和add
),运行完python3 xenc.py后从burpsuite的repeater中发上面的包并右键通过burpsuite测试有没有漏洞,或者将上面的request内容提供给sqlmap等工具进行测试