移动安全-Frida入门实战

Frida相关知识

frida是一款方便并且易用的跨平台Hook工具,使用它不仅可以Hook Java写的应用程序,而且还可以Hook原生的应用程序。

frida分客户端环境和服务端环境。在客户端我们可以编写Python代码,用于连接远程设备,提交要注入的代码到远程,接受服务端的发来的消息等。在服务端,我们需要用Javascript代码注入到目标进程,操作内存数据,给客户端发送消息等操作。我们也可以把客户端理解成控制端,服务端理解成被控端。假如我们要用PC来对Android设备上的某个进程进行操作,那么PC就是客户端,而Android设备就是服务端。

插桩技术】插桩技术是指将额外的代码注入程序中以收集运行时的信息,可分为两种:

(1)源代码插桩:额外代码注入到程序源代码中。

(2)二进制插桩:额外代码注入到二进制可执行文件中。

  • 静态二进制插桩:在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件。
  • 动态二进制插桩:在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。

关于HOOK技术的介绍、Frida的安装、Java反射的原理等内容本文不做阐述,请直接翻阅以下博文:移动安全-Hook技术。本文直接对Frida框架的基础用法(PC端命令和JS代码语法)进行介绍,然后进行入门实例的实战演示。

Frida基础用法

PC端命令

下面是Frida客户端(PC端)命令行的参数解释,看一下就好:

Usage: frida [options] target

Options:
  --version             show program's version number and exit
  -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
  -f FILE, --file=FILE  spawn FILE
  -n NAME, --attach-name=NAME
                        attach to NAME
  -p PID, --attach-pid=PID
                        attach to PID
  --debug               enable the Node.js compatible script debugger
  --enable-jit          enable JIT
  -l SCRIPT, --load=SCRIPT
                        load SCRIPT
  -c CODESHARE_URI, --codeshare=CODESHARE_URI
                        load CODESHARE_URI
  -e CODE, --eval=CODE  evaluate CODE
  -q                    quiet mode (no prompt) and quit after -l and -e
  --no-pause            automatically start main thread after startup
  -o LOGFILE, --output=LOGFILE
                        output to log file

如何将一个脚本注入到Android目标进程:

   frida -U -l myhook.js com.xxx.xxxx

参数解释:

  • -U 指定对USB设备操作
  • -l 指定加载一个Javascript脚本
  • 最后指定一个进程名,如果想指定进程pid,用-p选项。正在运行的进程可以用frida-ps -U命令查看

frida运行过程中,执行%resume重新注入,执行%reload来重新加载脚本;执行exit结束脚本注入。

JS代码语法

1、载入类

Java.use方法用于声明一个Java类,在用一个Java类之前首先得声明。比如声明一个String类,要指定完整的类名:

var StringClass=Java.use("java.lang.String");

2、类型转换

Java.cast方法来对一个对象进行类型转换,如将variable转换成java.lang.String:

var StringClass=Java.use("java.lang.String");
var NewTypeClass=Java.cast(variable,StringClass);

3、调用函数

和Java一样,创建类实例就是调用构造函数,而在这里用$new表示一个构造函数。

var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();

实例化以后调用其他函数:

var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();

4、Java.available字段

这个字段标记Java虚拟机(例如: Dalvik 或者 ART)是否已加载, 操作Java任何东西之前,要确认这个值是否为true

5、Java.perform方法

Java.perform(fn)在Javascript代码成功被附加到目标进程时调用,我们核心的代码要在里面写。格式:

Java.perform(function(){
    //do something...
});

6、修改函数的实现

修改一个函数的实现是逆向调试中相当有用的。修改一个函数的实现后,如果这个函数被调用,我们的Javascript代码里的函数实现也会被调用。

(1)函数参数类型表示

  • 对于基本类型,直接用它在Java中的表示方法就可以了,不用改变,例如:int、short、char、byte、boolean、float、double、long。
  • 基本类型数组,用左中括号接上基本类型的缩写。基本类型缩写表示表:在这里插入图片描述
  • 任意类,直接写完整类名即可。例如:java.lang.String
  • 对象数组,用左中括号接上完整类名再接上分号。例如:[java.lang.String;

(2)带参数的构造函数

修改参数为byte[]类型的构造函数的实现:

ClassName.$init.overload('[B').implementation=function(param){
    //do something
}

【注】:ClassName是使用Java.use定义的类;param是可以在函数体中访问的参数

修改多参数的构造函数的实现:

ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
    //do something
}

(3)无参数构造函数

ClassName.$init.overload().implementation=function(){
    //do something
}

调用原构造函数:

ClassName.$init.overload().implementation=function(){
    //do something
    this.$init();
    //do something
}

注意:当构造函数(函数)有多种重载形式,比如一个类中有两个形式的func:void func()void func(int),要加上overload来对函数进行重载,否则可以省略overload

(4)一般函数的修改

修改函数名为func,参数为byte[]类型的函数的实现:

ClassName.func.overload('[B').implementation=function(param){
    //do something
    //return ...
}

(5)无参数的函数的修改

ClassName.func.overload().implementation=function(){
    //do something
}

在修改函数实现时,如果原函数有返回值,那么我们在实现时也要返回合适的值。

ClassName.func.overload().implementation=function(){
    //do something
    return this.func();
}

Firda入门案例

JS修改返回值

【场景描述】

假设有以下的程序,给isExcellent方法传入两个值,通过计算,返回一个布尔值,表示是否智商达标。默认情况下,它是只会显示李二狗智商是否达标:false,因为我们默认传入的数很小:
在这里插入图片描述
具体代码如下:

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView =findViewById(R.id.tv);
        textView.setText("李二狗智商是否达标:"+isExcellent(10,20));
    }

    private  boolean isExcellent(int IQ1, int IQ2){
        if( IQ1 + IQ2 >=180){
            return true;
        }
        else{
            return false;
        }
    }
}

然后我们编写一个脚本来Hook isExcellent函数,使它返回true,显示为李二狗智商是否达标:true

【代码实现】对于这种简单的场景,直接修改返回值就可以了,因为只有结果是重要的。而想直接返回结果很简单,直接在匿名方法里return即可。JS代码如下:

if(Java.available){
    Java.perform(function(){
        var MainActivity = Java.use("com.sbw.atrue.fridatest.MainActivity");
        MainActivity.isExcellent.implementation=function(){
            return true;        
        }
    });
}

具体的HOOK过程:

  1. 将上面的代码保存为:test.js
  2. 将手机连接PC并通过adb shell命令,在PC中启动服务端:在这里插入图片描述
  3. 在手机中运行目标APP
  4. 在CMD中执行frida-ps -U查看手机中进程列表,获取目标APP进程名:在这里插入图片描述
  5. 执行frida -U -l D:\Code\Frida\APK1\test.js com.sbw.atrue.fridatest进行代码注入:在这里插入图片描述
  6. 按返回键返回桌面,再重新打开APP,发现达到预期注入效果,HOOK成功:在这里插入图片描述
  7. 在命令行输入exit,回车,停止注入代码:在这里插入图片描述

【注】:这里为什么要打开两次APP?第一次打开是为了让Frida能够找到进程,第二次打开是为了验证结果,即使Hook成功了,界面是有缓存的,并不能实时显示Hook结果,所以需要重新打开APP。

JS修改函数参数

以上HOOK篡改的情景过于简单。下面假设有以下场景,isExcellent除了返回是否智商是否达标以外,方法的内部还把智商总分数值给打印出来:
在这里插入图片描述
具体代码如下:

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView =findViewById(R.id.tv);
        textView.append("是否还能成功抢救:"+isExcellent(50,50)+"\n");
    }

    private  boolean isExcellent(int chinese, int math){
        textView.append("李二狗的智商总分:"+(chinese+math)+"\n");
        if( chinese + math >=180){
            return true;
        }
        else{
            return false;
        }
    }
}

这种情况下我们HOOK的时候不可能只返回是否能抢救吧,显示的总分很低,但是却返回可以抢救,是很尴尬的…….所以我们要修改isExcellent方法的参数,使其通过计算打印和返回合理的值。

if(Java.available){
    Java.perform(function(){
        var MainActivity = Java.use("com.sbw.atrue.fridatest.MainActivity");
        MainActivity.isExcellent.overload("int","int").implementation=function(IQ1,IQ2){
            return this.isExcellent(100,100);      
        }
    });
}

上面的代码,通过overload方法重载参数,修改isExcellent方法实现,并在实现函数里调用原来的方法,得到新的返回值。具体HOOK过程如下:

  1. 将上面的代码保存为:test2.js
  2. 将手机连接PC并通过adb shell命令,在PC中启动Frida服务端;
  3. 在手机中运行目标APP;
  4. 在CMD中执行frida-ps -U查看手机中进程列表,获取目标APP进程名;
  5. 执行frida -U -l D:\Code\Frida\APK2\test2.js com.sbw.atrue.fridatest进行代码注入;
  6. 按返回键返回桌面,再重新打开APP,发现达到预期注入效果,HOOK成功:在这里插入图片描述
  7. 在命令行输入exit,回车,停止注入代码。在这里插入图片描述

Python脚本注入

在本文刚开始的时候说到,我们可以编写Python代码来配合Javascript代码注入。下面我们来看看,怎么使用,先看一段代码:

import frida, sys

jscode = """
if(Java.available){
    Java.perform(function(){
        var MainActivity = Java.use("com.sbw.atrue.fridatest.MainActivity");
        MainActivity.isExcellent.overload("int","int").implementation=function(IQ1,IQ2){
            console.log("[*] Frida测试的JS代码已调用!");
            send("HOOK成功!");
            return this.isExcellent(100,100);      
        }
    });

}
"""

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)
pass

# 查找USB设备并附加到目标进程
session = frida.get_usb_device().attach('com.sbw.atrue.fridatest')
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
print('[*] Start attach')
# 加载创建好的javascript脚本
script.load()
# 读取系统输入
sys.stdin.read()

上面是一段Python代码,我们来分析它的步骤:

  1. 通过调用frida.get_usb_device()方法来得到一个连接中的USB设备(Device类)实例。
  2. 调用Device类的attach()方法来附加到目标进程并得到一个会话(Session类)实例,该方法有一个参数,参数是需要注入的进程名或者进程pid。(如果需要Hook的代码在APP的启动期执行,那么在调用attach方法前需要先调用Device类的spawn()方法,这个方法也有一个参数,参数是进程名,该方法调用后会重启对应的进程,并返回新的进程pid。得到新的进程pid后,我们可以将这个进程pid传递给attach()方法来实现附加)
  3. 接着调用Session类的create_script()方法创建一个脚本,传入需要注入的javascript代码并得到Script类实例。
  4. 调用Script类的on()方法添加一个消息回调,第一个参数是信号名,乖乖传入message就行,第二个是回调函数。
  5. 最后调用Script类的load()方法来加载刚才创建的脚本。

【注】:如果想在JavaScript中输出日志,可以调用console.log()方法。如果想给客户端发送消息,可以在JavaScript代码里调用send()方法,并在客户端Python代码里注册一个消息回调来接收服务端发来的消息。

最后,来看看利用Python脚本执行具体的HOOK过程:

  1. 将上面的POC代码,保存为pytest.py
  2. 将手机连接PC并通过adb shell命令,在PC中启动Frida服务端:
  3. 在手机中运行目标APP;
  4. 在CMD中执行python D:\Code\Frida\APK2\pytest.py进行代码注入:在这里插入图片描述
  5. 按返回键返回桌面,再重新打开APP,发现达到预期注入效果,HOOK成功:在这里插入图片描述
  6. Ctrl+C停止脚本和停止注入代码:在这里插入图片描述

从上面的案例我们可以看到,结合Python代码,可以使注入更加的灵活。最后附上本文的参考博文:Frida的用法—Hook Java代码篇

发布了117 篇原创文章 · 获赞 84 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/weixin_39190897/article/details/100517918