frida的用法--Hook Java代码篇

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

    frida_server

  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 [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结束脚本注入

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
  1. 基本类型数组,用左中括号接上基本类型的缩写

基本类型缩写表示表:

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

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

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

例如:java.lang.String

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

例如:[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的,因为我们默认传入的数很小:

exp1_before
public class MainActivity extends AppCompatActivity {
    private  String 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));
    }

    private  boolean 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,回车,停止注入代码

exp1_after

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

7.2 修改参数

7.2.1 场景

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

exp2_before
public class MainActivity extends AppCompatActivity {
    private  String 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");
    }

    private  boolean 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,回车,停止注入代码

exp2_after

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("[javascript] 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类)实例,该方法有一个参数,参数是需要注入的进程名或者进程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代码,使注入更加的灵活了。如果想看Python端frida模块的代码,可以访问:https://github.com/frida/frida-python/blob/master/frida/core.py

9. 参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容