frida用法

on under 二进制
6 minute read

0x0 About

记录适用于ios的objective-c的类中函数的frida的用法

0x1 必读

官方文档

API

官方文档部分翻译

利用FRIDA攻击Android应用程序1

利用FRIDA攻击Android应用程序2

如何在iOS应用程序中用Frida来绕过”越狱检测”

使用Frida配合Burp Suite追踪API调用

利用Frida从TeamViewer内存中提取密码

Brida:使用Frida进行移动应用渗透测试

联合Frida和BurpSuite的强大扩展–Brida

Object-C测试工具与Frida

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

classes.js

for (var className in ObjC.classes)
    {
        if (ObjC.classes.hasOwnProperty(className))
        {
            console.log(className);
        }
}

methodofclass.js

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");

record.js

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!");
}


overwrite.js

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函数

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()

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 调用函数

+号开头的函数(类函数,可直接通过类调用)要这样:

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()

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原理图

也即,brida调用frida的rpc后通过pyro开启一个rpc给安全人员写的burp插件用,目前笔者认为brida只能用于辅助对称加密(如aes)的加密后的数据,也即brida工作时,人工调用brida的解密功能解密数据后再修改数据,然后将修改后的数据再用brida的加密功能加密好,加密好之后再通过burpsuite发送给正常的远程服务器.如果要通过burpsuite来修改非对称加密(如rsa)的加密后的数据需要使用下面的方法

使用Frida配合Burp Suite追踪API调用

上面这个链接里的思路是这样的:

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中有两个加密函数encryptadd),运行完python3 xenc.py后从burpsuite的repeater中发上面的包并右键通过burpsuite测试有没有漏洞,或者将上面的request内容提供给sqlmap等工具进行测试

frida, inject, js
home
github
archive
category