luoyesiqiu 发表于 2019-4-16 21:58

是时候来了解frida的用法了--Hook Java代码篇

本帖最后由 luoyesiqiu 于 2019-4-20 07:51 编辑

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

# 1. 准备

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

## 1.1 准备frida服务端环境

本文,服务端在Android平台测试。服务端环境准备步骤如下:

1. 根据自己的平台下载frida服务端并解压

https://github.com/frida/frida/releases


2. 执行以下命令将服务端推到手机的/data/local/tmp目录

`adb push frida-server /data/local/tmp/frida-server`

3. 执行以下命令修改frida-server文件权限

`adb shell chmod 777 /data/local/tmp/frida-server`

> 注:Windows系统执行命令可以在CMD中进行;Linux和MacOS执行命令可以在终端中进行。adb是Android一个调试工具,具体安装方法不是本文的重点。

## 1.2 准备客户端环境

在PC上安装Python的运行环境,安装完成后执行下面的命令安装frida

`pip install frida`

## 1.3 客户端命令参数

下面是frida客户端命令行的参数解释,看一下就好

```
Usage: frida 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=HOSTconnect to remote frida-server on HOST
-f FILE, --file=FILEspawn 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=CODEevaluate 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`结束脚本注入

# 2. Hook Java方法

## 2.1 载入类

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

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

## 2.2 修改函数的实现

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

### 2.2.1 函数参数类型表示

不同的参数类型都有自己的表示方法

1. 对于基本类型,直接用它在Java中的表示方法就可以了,不用改变,例如:

* int
* short
* char
* byte
* boolean
* float
* double
* long

2. 基本类型数组,用左中括号接上基本类型的缩写

基本类型缩写表示表:

|基本类型 | 缩写 |
|---------|----------|
| boolean | Z |
| byte | B |
| char | C |
| double | D |
| float | F |
| int | I |
| long | J |
| short | S |

例如:`int[]`类型,在重载时要写成`[I`

3. 任意类,直接写完整类名即可

例如:`java.lang.String`

4.对象数组,用左中括号接上完整类名再接上分号

例如:`[java.lang.String;`

### 2.2.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
}
```

### 2.2.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

### 2.2.4 一般函数

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

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

### 2.2.5 无参数的函数

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

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

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

# 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.cast`方法来对一个对象进行类型转换,如将`variable`转换成`java.lang.String`:

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

# 5. Java.available字段

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


# 6. Java.perform方法

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

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

# 7. 实例讲解

有了以上的基础知识,我们就可以进行编写代码了

## 7.1 修改返回值

### 7.1.1 场景

假设有以下的程序,给isExcellent方法传入两个值,通过计算,返回一个布尔值,表示是否优秀。默认情况下,它是只会显示`是否优秀:false`的,因为我们默认传入的数很小:

```
public class MainActivity extends AppCompatActivity {
    privateString TAG="Crackme";
    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(46,54));
    }

    privateboolean isExcellent(int chinese, int math){
      if( chinese + math >=180){
            return true;
      }
      else{
            return false;
      }
    }

}
```
我们编写一个脚本来Hook isExcellent函数,使它返回true,显示为`是否优秀:true`

对于这种简单的场景,直接修改返回值就可以了,因为只有结果是重要的。

### 7.1.2 代码

想直接返回结果很简单,直接在匿名方法里return即可。
```
if(Java.available){
    Java.perform(function(){
      var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
      MainActivity.isExcellent.implementation=function(){
            return true;      
      }
    });

}
```

* 将上面的代码保存为:`exp1.js`

* 执行`adb shell 'su -c /data/local/tmp/frida-server'`启动服务端

* 运行目标App

* 执行`frida -U -l exp1.js com.luoyesiqiu.crackme`注入代码

* 按返回键返回桌面,再重新打开App,发现达到预期

* 在命令行输入`exit`,回车,停止注入代码


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

## 7.2 修改参数

### 7.2.1 场景

假设有以下场景,isExcellent除了返回是否优秀以外,方法的内部还把分数打印出来。


```
public class MainActivity extends AppCompatActivity {
    privateString TAG="Crackme";
    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(46,54)+"\n");
    }

    privateboolean isExcellent(int chinese, int math){
      textView.append("语文+数学总分:"+(chinese+math)+"\n");
      if( chinese + math >=180){
            return true;
      }
      else{
            return false;
      }
    }
}
```

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

### 7.2.2 代码

```
if(Java.available){
    Java.perform(function(){
      var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
      MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){
            return this.isExcellent(95,96);      
      }
    });

}
```

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

* 将上面的代码保存为:`exp2.js`

* 执行`adb shell 'su -c /data/local/tmp/frida-server'`启动服务端(如果上面启动的服务端还开着可省略这一步)

* 运行目标App

* 执行`frida -U -l exp2.js com.luoyesiqiu.crackme`注入代码

* 按返回键,再重新打开App,发现达到预期

* 在命令行输入`exit`,回车,停止注入代码


# 8. 配合Python脚本注入

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

```
# -*- coding: UTF-8 -*-

import frida, sys

jscode = """
if(Java.available){
    Java.perform(function(){
      var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
      MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){
            console.log(" isExcellent be called.");
            send("isExcellent be called.");
            return this.isExcellent(95,96);      
      }
    });

}
"""

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.luoyesiqiu.crackme')
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
print(' Start attach')
# 加载创建好的javascript脚本
script.load()
# 读取系统输入
sys.stdin.read()
```

* 将上面的代码,保存为`exp3.py`

* 执行`adb shell 'su -c /data/local/tmp/frida-server'`启动服务端(如果上面启动的服务端还开着可省略这一步)

* 运行目标App

* 执行`python exp3.py`注入代码

* 按返回键,再重新打开App,发现达到预期

* 按`Ctrl+C`停止脚本和停止注入代码

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

1. 通过调用`frida.get_usb_device()`方法来得到一个连接中的USB设备(Device类)实例
2. 调用Device类的`attach()`方法来附加到目标进程并得到一个会话(Session类)实例,该方法有一个参数,就是需要注入的进程名
3. 接着调用Session类的`create_script()`方法创建一个脚本,传入需要注入的javascript代码并得到Script类实例
4. 调用Script类的`on()`方法添加一个消息回调,第一个参数是信号名,乖乖传入`message`就行,第二个是回调函数
5. 最后调用Script类的`load()`方法来加载刚才创建的脚本。


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

可以看到,结合python代码,使注入更加的灵活了。如果想看Python端frida模块的代码,可以访问:https://github.com/frida/frida-python/blob/master/frida/core.py

# 9. 参考

* https://www.frida.re/docs/home/

luoyesiqiu 发表于 2019-4-18 17:44

ctw507 发表于 2019-4-18 09:57
你好 请问如何hook 按下按钮 比如登录 所加载的方法? 我知道有加载类的 就是不知道如何加载使用的方法...

如果是调用方法,用Java.use加载类,然后使用类名.$new实例化,生成的对象用[对象.方法名]调用。例如:
var Clazz=Java.use("xx.xx.Clazz");
var clazz = Clazz.$new();
clazz.func();

luoyesiqiu 发表于 2019-12-26 10:43

iwxiao 发表于 2019-12-4 14:58
博主您好,麻烦问下我再 安卓机上 运行 frida-server 提示:
Unable to load SELinux policy from the kern ...

尝试以root方式运行frida-server

52小将 发表于 2019-4-17 18:53

非常棒,感谢楼主。

vLove0 发表于 2019-4-17 09:31

很好的一篇文章,谢谢分享

莺歌燕语 发表于 2019-4-17 10:50

z这个东西是否需要root

sighforever 发表于 2019-4-17 11:25

感觉有点厉害啊

luoyesiqiu 发表于 2019-4-17 12:42

莺歌燕语 发表于 2019-4-17 10:50
z这个东西是否需要root

需要的,没有root会提示Unable to save SELinux policy to the kernel: Permission denied

xixicoco 发表于 2019-4-17 14:39

优秀,感谢大佬

洞见未来 发表于 2019-4-17 15:40

俺跑demo都会出错?是系统问题吗?
win10的,python 通过vs2017安装的,开发python相关程序都正常
就是frida试用就挂了.o(╯□╰)o

baifaqingsi 发表于 2019-4-17 15:48

感谢干货的分享

mayl8822 发表于 2019-4-17 17:56

下篇是不就是"是时候来了解frida的用法了--Hook Jni代码篇"
页: [1] 2 3 4 5
查看完整版本: 是时候来了解frida的用法了--Hook Java代码篇